Skip to content

Http.Sys response header serialization allocations #40215

@Tratcher

Description

@Tratcher

(offline customer report)

SerializeHeaders is allocating 1.09% memory, 0.63% for GetBytes, 0.32% for growing List. Allocation is certainly an issue here, but what worries me more is the possible contention for allocating and freeing GC handles.

Notice here the caller is calling GetBytes(string), so a new byte array is allocated every time. It would be more efficient to (re)use a single byte array, converting all strings into it, then pin the array once. The byte array can be reused, then no need for GC handles.

So I need to check caller code to see how it’s used. Here is the code:

            List<GCHandle> pinnedHeaders = SerializeHeaders(isOpaqueUpgrade);
            try
            {
                if (dataChunks != null)
                {
                    if (pinnedHeaders == null)
                    {
                        pinnedHeaders = new List<GCHandle>();
                    }
                    var handle = GCHandle.Alloc(dataChunks, GCHandleType.Pinned);
                    pinnedHeaders.Add(handle);
                    _nativeResponse.Response_V1.EntityChunkCount = (ushort)dataChunks.Length;
                    _nativeResponse.Response_V1.pEntityChunks = (HttpApiTypes.HTTP_DATA_CHUNK*)handle.AddrOfPinnedObject();
                }
                ...
           }
           finally
           {
               FreePinnedHeaders(pinnedHeaders);
           }

This contains all the lines using pinnedHeaders.

Here is one part within SerialiizeHeaders:

                                bytes = HeaderEncoding.GetBytes(headerName);
                                unknownHeaders[_nativeResponse.Response_V1.Headers.UnknownHeaderCount].NameLength = (ushort)bytes.Length;
                                gcHandle = GCHandle.Alloc(bytes, GCHandleType.Pinned);
                                pinnedHeaders.Add(gcHandle);
                               unknownHeaders[_nativeResponse.Response_V1.Headers.UnknownHeaderCount].pName = (byte*)gcHandle.AddrOfPinnedObject();

Here will be my suggestion:

internal unsafe class PinnedHeaders
{
    private List<GCHandle> pinnedHeaders;
    private byte[] buffer;
    private int bufferPos;
    private int bufferLength;
    private byte* pinnedBuffer;

    internal PinnedHeaders(byte[] buffer, byte * pinnedBuffer)
    {
        this.buffer = buffer;
        this.bufferLength = buffer.Length;
        this.pinnedBuffer = pinnedBuffer;
        this.pinnedHeaders = new List<GCHandle>();
    }

    internal byte* AddString(string text, Encoding encoding)
    {
        int length = encoding.GetByteCount(text);

        if ((this.bufferPos + length) < this.bufferLength)
        {
            encoding.GetBytes(text, 0, text.Length, buffer, this.bufferPos);

            var result = this.pinnedBuffer + this.bufferPos;

            this.bufferPos += length;

            return result;
        }

        byte[] encoded = encoding.GetBytes(text);

        var handle = GCHandle.Alloc(encoded, GCHandleType.Pinned);

        this.pinnedHeaders.Add(handle);

        return (byte*)handle.AddrOfPinnedObject();
    }

}

The byte[] buffer can be reused across calls. This should minimize the need for allocating GC handles for strings/byte[]).

Metadata

Metadata

Assignees

No one assigned

    Labels

    Perfarea-networkingIncludes servers, yarp, json patch, bedrock, websockets, http client factory, and http abstractionsfeature-httpsys

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions