Skip to content

Reduce per-request allocations and CPU in the Core request pipeline - #4566

Closed
martincostello wants to merge 9 commits into
aws:developmentfrom
martincostello:core-performance-improvements
Closed

martincostello wants to merge 9 commits into
aws:developmentfrom
martincostello:core-performance-improvements

Conversation

@martincostello

@martincostello martincostello commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Description

Reduces per-request CPU and allocations in AWSSDK.Core's request pipeline. Each change was profiled (dotnet-trace, GC allocation ticks, and a mocked-HTTP harness) and measured in isolation against a baseline build; only changes with a measurable win were kept.

One commit per logical change:

  1. Chunked upload buffers sized to the payload. ChunkedUploadWrapperStream allocated two ~80 KB buffers per request regardless of body size. Buffers are now sized to the known remaining length for plain streams (encrypting streams keep the full size), the read-and-copy path clamps reads to the space left in the chunk, the short final chunk is signed in place instead of being copied, and the chunk header is written directly into the output buffer.
  2. Lambda environment lookups cached. AWS_LAMBDA_FUNCTION_NAME / AWS_LAMBDA_MAX_CONCURRENCY are read once per process instead of on every request; the per-invocation trace id is still read per request.
  3. Telemetry no-op short-circuit. TracingUtilities.CreateSpan returns a shared no-op span when no tracer is registered, and HTTP telemetry recording is skipped when both the tracer and meter providers are no-ops.
  4. SigV4 hashing from pooled buffers (net8+). The canonical request and string-to-sign are encoded into a pooled buffer and hashed with the span APIs instead of allocating byte[] copies.
  5. Pooled JSON buffers returned after each response. StreamingUtf8JsonReader only returned its 4 KB pooled buffer when a caller read past the end of the stream, but generated unmarshallers stop at the root value's closing token, so every JSON response effectively allocated a new 4 KB array. The buffer is released when the root value closes, and the Unmarshaller handler disposes the context for non-streaming responses (returning its pooled path buffer too). Extracted to Return pooled JSON reader buffers after each response #4567.
  6. Endpoint resolution allocations. Services whose auth schemes come from the endpoint ruleset (S3) evaluated the ruleset twice per request; the first result is reused when the parameters are unchanged. Partition(region) builds one prototype bag per partition and hands out exact-size copies. Extracted to Reduce endpoint resolution allocations #4568.
  7. Logger lookup and debug-message guard. Logger.GetLogger no longer locks around its ConcurrentDictionary, and CanonicalizeResourcePathV2 only builds its debug message arguments when a debug logger is attached. HttpClientResponseData drops a redundant per-response HashSet. Extracted to Trim per-request logger overhead #4569.

Motivation and Context

Steady-state cost of a request through the pipeline (mocked HTTP, no network), branch vs development (9ab8dcf), best of three interleaved rounds. Ratios are branch ÷ baseline.

Operation Baseline min / median / alloc Branch min / median / alloc Time ratio (min / median) Alloc ratio
DynamoDB GetItem 19.8 µs / 20.9 µs / 28.70 KB 16.7 µs / 18.1 µs / 21.67 KB 0.84 / 0.87 0.76
SQS SendMessage 16.9 µs / 18.1 µs / 26.02 KB 14.9 µs / 16.2 µs / 19.00 KB 0.88 / 0.89 0.73
S3 PutObject (40 B body) 46.0 µs / 48.0 µs / 205.36 KB 28.3 µs / 29.7 µs / 35.30 KB 0.62 / 0.62 0.17
S3 ListBuckets 33.5 µs / 34.5 µs / 48.77 KB 27.1 µs / 28.5 µs / 39.86 KB 0.81 / 0.83 0.82

Allocation figures are exact (GC.GetAllocatedBytesForCurrentThread) and identical across rounds. Timings are per operation on an i7-13700H (process pinned to the P-cores), .NET 10 runtime against the net8.0 build of the SDK, after a 6 s warm-up so tiered compilation has settled; each row is the best of three interleaved baseline/branch rounds of 20 × 2000 operations. Individual commit messages record the effect measured for that commit alone.

Benchmark code

The harness is not checked in. It is a console project that references the SDK projects directly, replaces the HTTP client with a canned-response handler, and measures a full client call through the pipeline. Build it once from the default branch and copy bin/Release/net10.0 to CoreBench-base, then build it from the branch and run ab.sh "ddb sqs put list" 3.

CoreBench.csproj

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>disable</Nullable>
    <LangVersion>latest</LangVersion>
    <ServerGarbageCollection>false</ServerGarbageCollection>
    <TieredPGO>true</TieredPGO>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="BenchmarkDotNet" Version="0.15.2" />
  </ItemGroup>
  <ItemGroup>
    <ProjectReference Include="D:\Coding\aws\aws-sdk-net\sdk\src\Core\AWSSDK.Core.NetStandard.csproj" />
    <ProjectReference Include="D:\Coding\aws\aws-sdk-net\sdk\src\Services\DynamoDBv2\AWSSDK.DynamoDBv2.NetStandard.csproj" />
    <ProjectReference Include="D:\Coding\aws\aws-sdk-net\sdk\src\Services\S3\AWSSDK.S3.NetStandard.csproj" />
    <ProjectReference Include="D:\Coding\aws\aws-sdk-net\sdk\src\Services\SQS\AWSSDK.SQS.NetStandard.csproj" />
  </ItemGroup>
</Project>

Benchmarks.cs

using Amazon;
using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.Model;
using Amazon.Runtime;
using Amazon.S3;
using Amazon.S3.Model;
using Amazon.SQS;
using Amazon.SQS.Model;
using BenchmarkDotNet.Attributes;

namespace CoreBench;

[MemoryDiagnoser]
public class PipelineBenchmarks
{
    private AmazonDynamoDBClient _ddb;
    private AmazonS3Client _s3;
    private AmazonSQSClient _sqs;
    private GetItemRequest _getItem;
    private PutObjectRequest _putObject;
    private ListBucketsRequest _listBuckets;
    private SendMessageRequest _sendMessage;

    [GlobalSetup]
    public void Setup()
    {
        var creds = new BasicAWSCredentials("AKIAIOSFODNN7EXAMPLE", "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY");
        var factory = new MockHttpClientFactory();
        _ddb = new AmazonDynamoDBClient(creds, new AmazonDynamoDBConfig { RegionEndpoint = RegionEndpoint.USEast1, HttpClientFactory = factory });
        _s3 = new AmazonS3Client(creds, new AmazonS3Config { RegionEndpoint = RegionEndpoint.USEast1, HttpClientFactory = factory });
        _sqs = new AmazonSQSClient(creds, new AmazonSQSConfig { RegionEndpoint = RegionEndpoint.USEast1, HttpClientFactory = factory });

        _getItem = new GetItemRequest
        {
            TableName = "BenchmarkTable",
            Key = new Dictionary<string, AttributeValue>
            {
                ["pk"] = new AttributeValue { S = "id-1" },
                ["sk"] = new AttributeValue { S = "sort-1" },
            },
            ConsistentRead = true,
        };
        _putObject = new PutObjectRequest
        {
            BucketName = "my-bucket",
            Key = "some/key.txt",
            ContentBody = "hello world, this is a small object body",
            ContentType = "text/plain",
        };
        _listBuckets = new ListBucketsRequest();
        _sendMessage = new SendMessageRequest
        {
            QueueUrl = "https://sqs.us-east-1.amazonaws.com/123456789012/my-queue",
            MessageBody = MockHttpMessageHandler.SqsBody,
        };

        // warm once to fail fast on bad mock wiring
        DynamoGetItem().GetAwaiter().GetResult();
        S3PutObject().GetAwaiter().GetResult();
        S3ListBuckets().GetAwaiter().GetResult();
        SqsSendMessage().GetAwaiter().GetResult();
    }

    [Benchmark] public Task<GetItemResponse> DynamoGetItem() => _ddb.GetItemAsync(_getItem);
    [Benchmark] public Task<PutObjectResponse> S3PutObject() => _s3.PutObjectAsync(_putObject);
    [Benchmark] public PutObjectResponse S3PutObjectSync() => _s3.PutObjectAsync(_putObject).GetAwaiter().GetResult();
    [Benchmark] public Task<ListBucketsResponse> S3ListBuckets() => _s3.ListBucketsAsync(_listBuckets);
    [Benchmark] public Task<SendMessageResponse> SqsSendMessage() => _sqs.SendMessageAsync(_sendMessage);
}

MockHttp.cs

using System.Net;
using System.Security.Cryptography;
using System.Text;
using Amazon.Runtime;

namespace CoreBench;

public sealed class MockHttpClientFactory : HttpClientFactory
{
    public override HttpClient CreateHttpClient(IClientConfig clientConfig)
        => new HttpClient(new MockHttpMessageHandler(), disposeHandler: true);

    public override bool UseSDKHttpClientCaching(IClientConfig clientConfig) => true;
}

public sealed class MockHttpMessageHandler : HttpMessageHandler
{
    public const string SqsBody = "hello world";
    public static readonly byte[] DdbGetItemBytes = Encoding.UTF8.GetBytes(
        "{\"Item\":{\"pk\":{\"S\":\"id-1\"},\"sk\":{\"S\":\"sort-1\"},\"payload\":{\"S\":\"" + new string('a', 256) + "\"},\"count\":{\"N\":\"42\"},\"flag\":{\"BOOL\":true}}}");
    private static readonly byte[] SqsSendMessage = Encoding.UTF8.GetBytes(
        "{\"MD5OfMessageBody\":\"" + Convert.ToHexString(MD5.HashData(Encoding.UTF8.GetBytes(SqsBody))).ToLowerInvariant() + "\",\"MessageId\":\"5fea7756-0ea4-451a-a703-a558b933e274\"}");
    private static readonly byte[] S3ListBuckets = Encoding.UTF8.GetBytes(
        "<?xml version=\"1.0\" encoding=\"UTF-8\"?><ListAllMyBucketsResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"><Owner><ID>abc</ID><DisplayName>me</DisplayName></Owner><Buckets><Bucket><Name>bucket-a</Name><CreationDate>2020-01-01T00:00:00.000Z</CreationDate></Bucket><Bucket><Name>bucket-b</Name><CreationDate>2020-01-02T00:00:00.000Z</CreationDate></Bucket></Buckets></ListAllMyBucketsResult>");
    private static readonly byte[] Empty = Array.Empty<byte>();

    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var host = request.RequestUri.Host;
        HttpResponseMessage response;
        if (host.StartsWith("dynamodb", StringComparison.Ordinal))
        {
            response = new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(DdbGetItemBytes) };
            response.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/x-amz-json-1.0");
        }
        else if (host.StartsWith("sqs", StringComparison.Ordinal))
        {
            response = new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(SqsSendMessage) };
            response.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/x-amz-json-1.0");
        }
        else if (request.Method == HttpMethod.Put)
        {
            response = new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(Empty) };
            response.Headers.TryAddWithoutValidation("ETag", "\"9a0364b9e99bb480dd25e1f0284c8555\"");
        }
        else
        {
            response = new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(S3ListBuckets) };
            response.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/xml");
        }
        response.Headers.TryAddWithoutValidation("x-amzn-RequestId", "0123456789abcdef");
        response.Headers.TryAddWithoutValidation("x-amz-id-2", "abcdefghijklmnopqrstuvwxyz");
        response.RequestMessage = request;
        return Task.FromResult(response);
    }
}

Program.cs

using BenchmarkDotNet.Running;
using CoreBench;

if (args.Length > 0 && args[0] == "bench")
{
    // Hybrid CPU (P + E cores): keep the process on the performance cores so the single benchmark
    // thread is not scheduled onto an efficiency core mid-run. Override with BENCH_AFFINITY (hex mask).
    var mask = Environment.GetEnvironmentVariable("BENCH_AFFINITY");
    System.Diagnostics.Process.GetCurrentProcess().ProcessorAffinity =
        (IntPtr)(mask != null ? Convert.ToInt64(mask, 16) : 0xFFF);

    // Self-contained steady-state harness: time-based warm-up (so tiered compilation settles), then
    // measures blocks and reports min / median / mean per operation plus allocated bytes per operation.
    var b = new PipelineBenchmarks();
    b.Setup();
    var which = args.Length > 1 ? args[1] : "put";
    var warmupSeconds = args.Length > 2 ? double.Parse(args[2]) : 6;
    var blocks = args.Length > 3 ? int.Parse(args[3]) : 20;
    var blockIters = args.Length > 4 ? int.Parse(args[4]) : 2000;
    Func<Task> op = which switch
    {
        "ddb" => () => b.DynamoGetItem(),
        "put" => () => b.S3PutObject(),
        "list" => () => b.S3ListBuckets(),
        "sqs" => () => b.SqsSendMessage(),
        _ => throw new ArgumentException(which),
    };
    var sw = System.Diagnostics.Stopwatch.StartNew();
    long warmIters = 0;
    while (sw.Elapsed.TotalSeconds < warmupSeconds || warmIters < 20000)
    {
        op().GetAwaiter().GetResult();
        warmIters++;
    }
    var results = new List<double>();
    long allocBefore = GC.GetAllocatedBytesForCurrentThread();
    for (int k = 0; k < blocks; k++)
    {
        var t0 = System.Diagnostics.Stopwatch.GetTimestamp();
        for (int i = 0; i < blockIters; i++) op().GetAwaiter().GetResult();
        var t1 = System.Diagnostics.Stopwatch.GetTimestamp();
        results.Add((t1 - t0) * 1_000_000.0 / System.Diagnostics.Stopwatch.Frequency / blockIters);
    }
    long allocAfter = GC.GetAllocatedBytesForCurrentThread();
    results.Sort();
    var median = results[results.Count / 2];
    var mean = results.Average();
    var allocPerOp = (allocAfter - allocBefore) / (double)(blocks * blockIters);
    Console.WriteLine($"{which,-5} min {results[0],7:F2} us  median {median,7:F2} us  mean {mean,7:F2} us  alloc {allocPerOp / 1024,7:F2} KB  (warmup {warmIters} iters)");
    return;
}

// BenchmarkDotNet (use --inProcess so the saved baseline binaries are the ones measured)
BenchmarkSwitcher.FromAssembly(typeof(PipelineBenchmarks).Assembly).Run(args);

ab.sh

#!/bin/bash
# Usage: ab.sh "<ops>" [rounds] [warmupSeconds]
# Runs the harness for the baseline build (CoreBench-base, built from the default branch)
# and the candidate build alternately, so machine noise affects both the same way.
BASE="./CoreBench-base/CoreBench.exe"
NEW="./CoreBench/bin/Release/net10.0/CoreBench.exe"
OPS="$1"; ROUNDS="${2:-2}"; WARM="${3:-6}"
for op in $OPS; do
  for r in $(seq 1 $ROUNDS); do
    for v in base new; do
      if [ $v = base ]; then exe="$BASE"; else exe="$NEW"; fi
      echo "[$v r$r] $("$exe" bench $op $WARM 20 2000 | tail -1)"
    done
  done
done

Testing

  • AWSSDK.Core.NetStandard.csproj and AWSSDK.Core.NetFramework.csproj build for all targets with no warnings.
  • AWSSDK.UnitTests.Core on net472: 1562 passed, 2 failed (DefaultCredentialsResolverTests.CredentialsAreReevaluated*), and those two fail identically on development on the same machine because they depend on local credential state.
  • The net8.0 test target compiles but could not be executed locally (MSTest adapter/framework mismatch in this environment), so the net8-only code paths and the new HttpClientResponseDataTests rely on CI for their first run.
  • New tests: chunk buffer sizing (exact-size, full-size for non-seekable and encrypting streams), the read-and-copy clamp with an oversized read buffer, multi-chunk round-trip with chunk signatures recomputed independently, endpoint cache hit / miss / null parameters, early pooled-buffer release with invalid bytes after the root value, and response header ordering and lookups.
  • Two existing tests were updated to reset the new Lambda environment cache after changing environment variables.

Dry-runs

  • DotNet Dry-run ID: N/A (external contribution)
    • Pending
    • Completed successfully
    • Failed
  • PowerShell Dry-run ID: N/A (external contribution)
    • Pending
    • Completed successfully
    • Failed

Breaking Changes Assessment

  1. No public API changes. Behavioral notes:
    • ChunkedUploadWrapperStream buffer sizes are now payload-dependent for seekable, non-encrypting streams; the emitted aws-chunked content is unchanged (tests pin chunk sizes and signatures).
    • The Lambda function name / max-concurrency environment variables are read once per process. The Lambda runtime sets them before user code runs, so deployed behavior is unchanged; tests that toggle them use an internal reset hook.
    • StreamingUtf8JsonReader.Read returns false after the root value's closing token instead of attempting to read past it; reading further would previously have thrown on trailing non-JSON bytes.
  2. Reviewer assignment is at the maintainers' discretion.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist

  • My code follows the code style of this project
  • My change requires a change to the documentation
  • I have updated the documentation accordingly
  • I have read the README document
  • I have added tests to cover my changes
  • All new and existing tests passed

License

  • I confirm that this pull request can be released under the Apache 2 license

This change was generated by AI tools (Claude Code), and reviewed by Martin Costello (@martincostello).

🤖 Generated with Claude Code

ChunkedUploadWrapperStream always allocated two buffers of DefaultChunkSize
(~80 KB each) even when the wrapped stream was far smaller. When the remaining
length of the wrapped stream is known and smaller than a chunk, size the input
and output buffers to it instead.

Measured with a mocked HTTP client: S3 PutObject with a 40-byte body drops
from 205 KB to 45 KB allocated per request and is about 25% faster.
AWS_LAMBDA_FUNCTION_NAME and AWS_LAMBDA_MAX_CONCURRENCY are fixed for the
lifetime of a Lambda process, but the marshaller read them from the
environment on every request. Read them once and keep reading the
per-invocation _X_AMZN_TRACE_ID per request. Tests that toggle the variables
reset the cache through an internal hook.
TracingUtilities.CreateSpan built an attribute set, scope string, tracer and
span for every call even with the default no-op TracerProvider, and the HTTP
handler recorded request/response telemetry into them. Return a shared no-op
span when the no-op provider is in use and skip RecordHttpTelemetryData when
both tracing and metrics are no-ops.

Saves about 1.5 KB of allocations per request.
ComputeHash(string) and ComputeKeyedHash(string) encoded the canonical request
and string-to-sign into fresh byte arrays for every request. On .NET 8+ encode
into a pooled buffer and hash the span with SHA256.HashData /
HMACSHA256.HashData instead. Other targets and algorithms keep the existing
path.
StreamingUtf8JsonReader only returned its pooled buffer when a caller read past
the end of the stream, but generated unmarshallers stop at the closing token of
the root value, so every JSON response rented (and effectively allocated) a new
4 KB buffer. Release the buffer once the root value's closing token has been
read and guard against reading or double-returning afterwards.

The Unmarshaller handler also now disposes the unmarshaller context for
non-streaming responses, which returns the context's own pooled path buffer and
closes its readers.

Unmarshalling a small DynamoDB GetItem response drops from 6.5 KB to 2.4 KB;
DynamoDB and SQS requests allocate 16-18% less and run measurably faster.
Services whose auth schemes come from the endpoint ruleset (such as S3) resolve
the endpoint in the auth resolver and again in the endpoint resolver. Keep the
result on the request context and reuse it when the endpoint parameters are
unchanged, re-applying only the URL normalization.

Partition(region) built a six-entry property bag (with dictionary growth and
boxed booleans) on every call. Build one prototype per partition shape and hand
out exact-size copies so callers still get their own instance.

S3 PutObject allocates 16% less and is about 10% faster; ListBuckets allocates
13% less.
Logger.GetLogger took a lock around a ConcurrentDictionary lookup on every
call; use GetOrAdd. CanonicalizeResourcePathV2 built the arguments for a debug
message on every signed request; only build them when a debug logger is
attached. On .NET 8+ copy response headers through HttpHeaders.NonValidated so
the header values are not parsed and validated for every response.
ChunkedUploadWrapperStream
- Keep full-size buffers for encrypting (read-and-copy) streams, whose
  reported length can exceed Length - Position, and clamp every
  read-and-copy read to the space left in the chunk buffer so it can no
  longer overflow. The interim read buffer is allocated once, sized to
  min(buffer size, chunk size).
- Treat ObjectDisposedException and IOException from the length probe
  like NotSupportedException so a bad stream still fails at first read.
- Drop the exact-size copy of a short final chunk: SigV4 hashes only the
  bytes read through a new internal string-to-sign overload (the public
  overload keeps its contract and delegates to it), and SigV4a signs a
  MemoryStream slice. The chunk header is written straight into the
  output buffer without a StringBuilder or temporary byte arrays, the
  signing key is captured once instead of copied per chunk, and the
  empty-body hash uses the existing constant.
- Expose InputBufferSize for tests.

StreamingUtf8JsonReader
- Null the buffer reference once it is returned to the pool and reject
  refills explicitly, so misuse after the root value fails fast.

BaseEndpointResolver
- Only cache a resolved endpoint when parameters were mapped, and guard
  the cache lookup against a null parameter set (the base resolver maps
  none). Remove a dead null check.

HttpClientResponseData
- Revert the .NET 8 NonValidated header read so every target returns the
  first parsed header value as before. Use the enumerated values instead
  of a second lookup per header, and drop the per-response HashSet in
  favour of the case-insensitive header dictionary.

Tests cover the exact-size and full-size chunk buffers, the read-and-copy
clamp with an encrypting stream, multi-chunk round-tripping with chunk
signatures recomputed independently, the endpoint cache hit/miss/null
paths, the early pooled-buffer release (with invalid bytes after the root
value), and response header ordering and lookups.
@martincostello
martincostello marked this pull request as ready for review October 26, 2026 23:39
@martincostello
martincostello requested review from a team as code owners October 26, 2026 23:39
@martincostello
martincostello requested review from a team, boblodgett, dscpinheiro and jterapin and a balanced review from Copilot and removed request for a team October 26, 2026 23:39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

@jnunn-aws

jnunn-aws commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Thanks, Martin. This is well organized and well measured.

Several of these changes overlap draft PRs we already have open:

The JSON pooled-buffer release, the endpoint reuse with the Partition prototype, and the logger trims aren't covered by any of those. Could you split those three into separate PRs? That way each can be reviewed and measured on its own, without conflicting with the drafts above.

*Edited to remove prior suggestion to target feature/4.1 branch.

@martincostello

martincostello commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor Author

The JSON pooled-buffer release, the endpoint reuse with the Partition prototype, and the logger trims aren't covered by any of those. Could you split those three into separate PRs?

Created #4567, #4568 and #4569 for those three changes.

@martincostello

Copy link
Copy Markdown
Contributor Author

I also created #4570 which builds on top of the same ideas as #4533 with some fixes for findings during a local review loop of 56aa152.

@jnunn-aws

Copy link
Copy Markdown
Contributor

Thanks again, Martin! Everything here is now covered elsewhere: the JSON buffer, endpoint reuse and logger changes in #4567–#4569, chunked uploads in #4570, and the rest in our #4558, #4559, #4561, #4563 and #4533. I'm closing this in favor of those.

@jnunn-aws jnunn-aws closed this Oct 28, 2026
@martincostello
martincostello deleted the core-performance-improvements branch October 29, 2026 06:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants