Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use Utf8BufferTextWriter in MinimalApis view rendering #669

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
204 changes: 204 additions & 0 deletions Fluid/Utils/Utf8BufferTextWriter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Text;
#if !NETCOREAPP
using System.Runtime.InteropServices;
#endif

namespace Fluid.Utils
{
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

public sealed class Utf8BufferTextWriter : TextWriter
{
private static readonly UTF8Encoding _utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
private const int MaximumBytesPerUtf8Char = 4;

[ThreadStatic]
private static Utf8BufferTextWriter _cachedInstance;

private readonly Encoder _encoder;
private IBufferWriter<byte> _bufferWriter;
private Memory<byte> _memory;
private int _memoryUsed;

#if DEBUG
private bool _inUse;
#endif

public override Encoding Encoding => _utf8NoBom;

public Utf8BufferTextWriter()
{
_encoder = _utf8NoBom.GetEncoder();
}

public static Utf8BufferTextWriter Get(IBufferWriter<byte> bufferWriter)
{
var writer = _cachedInstance;
writer ??= new Utf8BufferTextWriter();

// Taken off the thread static
_cachedInstance = null;
#if DEBUG
if (writer._inUse)
{
throw new InvalidOperationException("The writer wasn't returned!");
}

writer._inUse = true;
#endif
writer.SetWriter(bufferWriter);
return writer;
}

public static void Return(Utf8BufferTextWriter writer)
{
_cachedInstance = writer;

writer._encoder.Reset();
writer._memory = Memory<byte>.Empty;
writer._memoryUsed = 0;
writer._bufferWriter = null;

#if DEBUG
writer._inUse = false;
#endif
}

public void SetWriter(IBufferWriter<byte> bufferWriter)
{
_bufferWriter = bufferWriter;
}

public override void Write(char[] buffer, int index, int count)
{
WriteInternal(buffer.AsSpan(index, count));
}

public override void Write(char[] buffer)
{
if (buffer is not null)
{
WriteInternal(buffer);
}
}

public override void Write(char value)
{
if (value <= 127)
{
EnsureBuffer();

// Only need to set one byte
// Avoid Memory<T>.Slice overhead for perf
_memory.Span[_memoryUsed] = (byte)value;
_memoryUsed++;
}
else
{
WriteMultiByteChar(value);
}
}

private unsafe void WriteMultiByteChar(char value)
{
var destination = GetBuffer();

// Json.NET only writes ASCII characters by themselves, e.g. {}[], etc
// this should be an exceptional case
#if NETCOREAPP
_encoder.Convert(new Span<char>(&value, 1), destination, false, out _, out _, out _);
#else
fixed (byte* destinationBytes = &MemoryMarshal.GetReference(destination))
{
_encoder.Convert(&value, 1, destinationBytes, destination.Length, false, out _, out _, out _);
}
#endif

_memoryUsed += 0;
}

public override void Write(string value)
{
if (value is not null)
{
WriteInternal(value.AsSpan());
}
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private Span<byte> GetBuffer()
{
EnsureBuffer();

return _memory.Span.Slice(_memoryUsed, _memory.Length - _memoryUsed);
}

private void EnsureBuffer()
{
// 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)
{
_bufferWriter!.Advance(_memoryUsed);
}

_memory = _bufferWriter!.GetMemory(MaximumBytesPerUtf8Char);
_memoryUsed = 0;
}
}

private void WriteInternal(ReadOnlySpan<char> buffer)
{
while (buffer.Length > 0)
{
// The destination byte array might not be large enough so multiple writes are sometimes required
var destination = GetBuffer();

#if NETCOREAPP
_encoder.Convert(buffer, destination, false, out var charsUsed, out var bytesUsed, out _);
#else
unsafe
{
fixed (char* sourceChars = &MemoryMarshal.GetReference(buffer))
fixed (byte* destinationBytes = &MemoryMarshal.GetReference(destination))
{
_encoder.Convert(sourceChars, buffer.Length, destinationBytes, destination.Length, false, out var charsUsed, out var bytesUsed, out _);
}
}
#endif

buffer = buffer.Slice(0);
_memoryUsed += 0;
}
}

public override void Flush()
{
if (_memoryUsed > 0)
{
_bufferWriter!.Advance(_memoryUsed);
_memory = _memory.Slice(_memoryUsed);
_memoryUsed = 0;
}
}

protected override void Dispose(bool disposing)
{
base.Dispose(disposing);

if (disposing)
{
Flush();
}
}
}
}
13 changes: 11 additions & 2 deletions MinimalApis.LiquidViews/ActionViewResult.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Fluid;
using Fluid.Utils;
using Fluid.ViewEngine;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
Expand Down Expand Up @@ -50,8 +51,16 @@ public async Task ExecuteAsync(HttpContext httpContext)
var context = new TemplateContext(_model, options.TemplateOptions);
context.Options.FileProvider = options.PartialsFileProvider;

await using var sw = new StreamWriter(httpContext.Response.Body);
await fluidViewRenderer.RenderViewAsync(sw, viewPath, context);
var textWriter = Utf8BufferTextWriter.Get(httpContext.Response.BodyWriter);
try
{
await fluidViewRenderer.RenderViewAsync(textWriter, viewPath, context);
await textWriter.FlushAsync();
}
finally
{
Utf8BufferTextWriter.Return(textWriter);
}
}

private static string LocatePageFromViewLocations(string viewName, FluidViewEngineOptions options)
Expand Down