-
Notifications
You must be signed in to change notification settings - Fork 381
[http-client-csharp] Add Tier 1 in-memory response cache to playground-server #10718
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
Merged
JoshLove-msft
merged 18 commits into
main
from
copilot/improve-csharp-emitter-performance
Jun 3, 2026
Merged
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
5f152e4
Initial plan
Copilot 3c68428
[http-client-csharp] Add Tier 1 in-memory response cache to playgroun…
Copilot 48ddd6c
Remove issue-pertaining comments and redundant inline comments
Copilot 087a1cb
Merge remote-tracking branch 'origin/main' into copilot/improve-cshar…
Copilot 79bae62
Remove sliding expiration from generation cache
Copilot e1bf971
Add telemetry for cache hits
Copilot 7b32d98
Model cache status as telemetry dimension instead of outcome
JoshLove-msft ee0f170
Use GenerateOutcome enum for /generate telemetry outcomes
JoshLove-msft d0dffd3
Add ComputeKey tests with a realistic code model
JoshLove-msft 4b4016f
Move sample code model to TestData files instead of inlining
JoshLove-msft 6543d36
Apply prettier formatting to TestData JSON
JoshLove-msft 8ffa9f4
Merge remote-tracking branch 'upstream/main' into copilot/improve-csh…
JoshLove-msft 9a158c9
Address review: split cache types, configurable size limit, central p…
JoshLove-msft 79fc189
Constrain 'which' catalog dependency to ^6.0.1
JoshLove-msft 7bf3519
Revert "Constrain 'which' catalog dependency to ^6.0.1"
JoshLove-msft 147c085
Use checkLatest for NodeTool to install latest 22.x/24.x
JoshLove-msft e793e23
Harden cache eviction test assertions
JoshLove-msft ae50df7
Merge remote-tracking branch 'upstream/main' into copilot/improve-csh…
JoshLove-msft File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
216 changes: 216 additions & 0 deletions
216
packages/http-client-csharp/playground-server.Tests/GenerationCacheTests.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,216 @@ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||
| // Licensed under the MIT License. | ||
|
|
||
| using System; | ||
| using System.IO; | ||
| using System.Text; | ||
| using Microsoft.Extensions.Caching.Memory; | ||
| using NUnit.Framework; | ||
| using PlaygroundServer; | ||
|
|
||
| namespace PlaygroundServer.Tests; | ||
|
|
||
| [TestFixture] | ||
| public class GenerationCacheTests | ||
| { | ||
| private static MemoryGenerationCache CreateCache(long sizeLimit = MemoryGenerationCache.DefaultSizeLimitBytes) | ||
| { | ||
| var memory = new MemoryCache(new MemoryCacheOptions { SizeLimit = sizeLimit }); | ||
| return new MemoryGenerationCache(memory); | ||
| } | ||
|
|
||
| private static CachedGenerationResponse MakeResponse(string content) | ||
| => new(Encoding.UTF8.GetBytes(content), "application/json"); | ||
|
|
||
| // Reads a file from TestData/<this fixture>/, mirroring the generator test projects' TestData convention. | ||
| // Files are copied next to the test assembly via the csproj CopyToOutputDirectory item. | ||
| private static string ReadTestData(string fileName) | ||
| => File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "TestData", nameof(GenerationCacheTests), fileName)); | ||
|
|
||
| // A representative (multi-KB) code model payload shaped like what the generator actually receives, | ||
| // used to exercise ComputeKey against realistic input rather than tiny synthetic strings. | ||
| private static string SampleCodeModel => ReadTestData("sample-codemodel.json"); | ||
|
|
||
| private static string SampleConfiguration => ReadTestData("sample-configuration.json"); | ||
|
|
||
| [Test] | ||
| public void ComputeKey_IsDeterministic_ForSameInputs() | ||
| { | ||
| var k1 = MemoryGenerationCache.ComputeKey("gen", "{\"a\":1}", "{\"b\":2}", "1.0.0"); | ||
| var k2 = MemoryGenerationCache.ComputeKey("gen", "{\"a\":1}", "{\"b\":2}", "1.0.0"); | ||
| Assert.AreEqual(k1, k2); | ||
| } | ||
|
|
||
| [Test] | ||
| public void ComputeKey_ProducesSha256HexString() | ||
| { | ||
| var key = MemoryGenerationCache.ComputeKey("gen", "model", "config", "1.0.0"); | ||
| // SHA-256 hex = 64 hex chars, uppercase per Convert.ToHexString. | ||
| Assert.AreEqual(64, key.Length); | ||
| Assert.That(key, Does.Match("^[0-9A-F]{64}$")); | ||
| } | ||
|
|
||
| [Test] | ||
| public void ComputeKey_ChangesWhenGeneratorNameChanges() | ||
| { | ||
| var a = MemoryGenerationCache.ComputeKey("genA", "m", "c", "v"); | ||
| var b = MemoryGenerationCache.ComputeKey("genB", "m", "c", "v"); | ||
| Assert.AreNotEqual(a, b); | ||
| } | ||
|
|
||
| [Test] | ||
| public void ComputeKey_ChangesWhenCodeModelChanges() | ||
| { | ||
| var a = MemoryGenerationCache.ComputeKey("gen", "m1", "c", "v"); | ||
| var b = MemoryGenerationCache.ComputeKey("gen", "m2", "c", "v"); | ||
| Assert.AreNotEqual(a, b); | ||
| } | ||
|
|
||
| [Test] | ||
| public void ComputeKey_ChangesWhenConfigurationChanges() | ||
| { | ||
| var a = MemoryGenerationCache.ComputeKey("gen", "m", "c1", "v"); | ||
| var b = MemoryGenerationCache.ComputeKey("gen", "m", "c2", "v"); | ||
| Assert.AreNotEqual(a, b); | ||
| } | ||
|
|
||
| [Test] | ||
| public void ComputeKey_ChangesWhenGeneratorVersionChanges() | ||
| { | ||
| var a = MemoryGenerationCache.ComputeKey("gen", "m", "c", "1.0.0"); | ||
| var b = MemoryGenerationCache.ComputeKey("gen", "m", "c", "1.0.1"); | ||
| Assert.AreNotEqual(a, b); | ||
| } | ||
|
|
||
| [Test] | ||
| public void ComputeKey_IsUnambiguousAcrossComponentBoundaries() | ||
| { | ||
| // Naive concatenation would collide here; the length prefix must prevent that. | ||
| var a = MemoryGenerationCache.ComputeKey("Foo", "Bar", "Baz", "v"); | ||
| var b = MemoryGenerationCache.ComputeKey("FooBar", "", "Baz", "v"); | ||
| var c = MemoryGenerationCache.ComputeKey("Foo", "BarBaz", "", "v"); | ||
| Assert.AreNotEqual(a, b); | ||
| Assert.AreNotEqual(a, c); | ||
| Assert.AreNotEqual(b, c); | ||
| } | ||
|
|
||
| [Test] | ||
| public void ComputeKey_WithRealisticCodeModel_IsDeterministic() | ||
| { | ||
| var k1 = MemoryGenerationCache.ComputeKey("ScmCodeModelGenerator", SampleCodeModel, SampleConfiguration, "1.0.0"); | ||
| var k2 = MemoryGenerationCache.ComputeKey("ScmCodeModelGenerator", SampleCodeModel, SampleConfiguration, "1.0.0"); | ||
| Assert.AreEqual(k1, k2); | ||
| Assert.AreEqual(64, k1.Length); | ||
| Assert.That(k1, Does.Match("^[0-9A-F]{64}$")); | ||
| } | ||
|
|
||
| [Test] | ||
| public void ComputeKey_WithRealisticCodeModel_ChangesOnSemanticEdit() | ||
| { | ||
| // Flip a single property name deep in the model; the key must change. | ||
| var edited = SampleCodeModel.Replace("\"name\": \"name\"", "\"name\": \"fullName\""); | ||
| Assert.AreNotEqual(SampleCodeModel, edited, "precondition: the edit must alter the code model"); | ||
|
|
||
| var original = MemoryGenerationCache.ComputeKey("ScmCodeModelGenerator", SampleCodeModel, SampleConfiguration, "1.0.0"); | ||
| var afterEdit = MemoryGenerationCache.ComputeKey("ScmCodeModelGenerator", edited, SampleConfiguration, "1.0.0"); | ||
| Assert.AreNotEqual(original, afterEdit); | ||
| } | ||
|
|
||
| [Test] | ||
| public void ComputeKey_WithRealisticCodeModel_ChangesOnConfigurationEdit() | ||
| { | ||
| var editedConfig = SampleConfiguration.Replace("\"library-name\": \"PetStore\"", "\"library-name\": \"PetStoreV2\""); | ||
| Assert.AreNotEqual(SampleConfiguration, editedConfig, "precondition: the edit must alter the configuration"); | ||
|
|
||
| var original = MemoryGenerationCache.ComputeKey("ScmCodeModelGenerator", SampleCodeModel, SampleConfiguration, "1.0.0"); | ||
| var afterEdit = MemoryGenerationCache.ComputeKey("ScmCodeModelGenerator", SampleCodeModel, editedConfig, "1.0.0"); | ||
| Assert.AreNotEqual(original, afterEdit); | ||
| } | ||
|
|
||
| [Test] | ||
| public void ComputeKey_RealisticCodeModel_RoundTripsThroughCache() | ||
| { | ||
| var cache = CreateCache(); | ||
| var key = MemoryGenerationCache.ComputeKey("ScmCodeModelGenerator", SampleCodeModel, SampleConfiguration, "1.0.0"); | ||
|
|
||
| Assert.IsFalse(cache.TryGet(key, out _)); | ||
| cache.Set(key, MakeResponse("generated")); | ||
|
|
||
| Assert.IsTrue(cache.TryGet(key, out var value)); | ||
| Assert.AreEqual("generated", Encoding.UTF8.GetString(value!.Body)); | ||
| } | ||
|
|
||
| [Test] | ||
| public void ComputeKey_ThrowsOnNullArguments() | ||
| { | ||
| Assert.Throws<ArgumentNullException>(() => MemoryGenerationCache.ComputeKey(null!, "m", "c", "v")); | ||
| Assert.Throws<ArgumentNullException>(() => MemoryGenerationCache.ComputeKey("g", null!, "c", "v")); | ||
| Assert.Throws<ArgumentNullException>(() => MemoryGenerationCache.ComputeKey("g", "m", null!, "v")); | ||
| Assert.Throws<ArgumentNullException>(() => MemoryGenerationCache.ComputeKey("g", "m", "c", null!)); | ||
| } | ||
|
|
||
| [Test] | ||
| public void TryGet_ReturnsFalse_WhenKeyMissing() | ||
| { | ||
| var cache = CreateCache(); | ||
| Assert.IsFalse(cache.TryGet("nope", out var value)); | ||
| Assert.IsNull(value); | ||
| } | ||
|
|
||
| [Test] | ||
| public void Set_ThenTryGet_ReturnsStoredValue() | ||
| { | ||
| var cache = CreateCache(); | ||
| var response = MakeResponse("hello"); | ||
| cache.Set("k", response); | ||
|
|
||
| Assert.IsTrue(cache.TryGet("k", out var value)); | ||
| Assert.IsNotNull(value); | ||
| Assert.AreEqual("application/json", value!.ContentType); | ||
| Assert.AreEqual("hello", Encoding.UTF8.GetString(value.Body)); | ||
| } | ||
|
|
||
| [Test] | ||
| public void Set_OverwritesExistingEntry() | ||
| { | ||
| var cache = CreateCache(); | ||
| cache.Set("k", MakeResponse("first")); | ||
| cache.Set("k", MakeResponse("second")); | ||
|
|
||
| Assert.IsTrue(cache.TryGet("k", out var value)); | ||
| Assert.AreEqual("second", Encoding.UTF8.GetString(value!.Body)); | ||
| } | ||
|
|
||
| [Test] | ||
| public void Set_ThrowsOnNullValue() | ||
| { | ||
| var cache = CreateCache(); | ||
| Assert.Throws<ArgumentNullException>(() => cache.Set("k", null!)); | ||
| } | ||
|
|
||
| [Test] | ||
| public void Constructor_ThrowsOnNullBackingCache() | ||
| { | ||
| Assert.Throws<ArgumentNullException>(() => new MemoryGenerationCache(null!)); | ||
| } | ||
|
|
||
| [Test] | ||
| public void SizeLimit_EvictsEntriesUnderPressure() | ||
| { | ||
| using var backing = new MemoryCache(new MemoryCacheOptions { SizeLimit = 1024, CompactionPercentage = 0.5 }); | ||
| var cache = new MemoryGenerationCache(backing); | ||
|
|
||
| var payload = new byte[256]; | ||
| for (int i = 0; i < 8; i++) | ||
| { | ||
| cache.Set("k" + i, new CachedGenerationResponse(payload, "application/json")); | ||
| } | ||
|
|
||
| backing.Compact(0.0); | ||
|
|
||
| // With a 1024-byte size limit and 8 entries of 256 bytes each, the cache must | ||
| // evict enough entries to stay within the configured budget. | ||
| Assert.That(backing.Count, Is.LessThan(8)); | ||
| Assert.That(backing.Count * 256, Is.LessThanOrEqualTo(1024)); | ||
| } | ||
| } | ||
77 changes: 77 additions & 0 deletions
77
...client-csharp/playground-server.Tests/TestData/GenerationCacheTests/sample-codemodel.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| { | ||
| "$id": "1", | ||
| "name": "PetStore", | ||
| "apiVersions": ["2024-01-01"], | ||
| "enums": [ | ||
| { | ||
| "$id": "2", | ||
| "kind": "enum", | ||
| "name": "PetKind", | ||
| "valueType": { | ||
| "$id": "3", | ||
| "kind": "string", | ||
| "name": "string", | ||
| "crossLanguageDefinitionId": "TypeSpec.string" | ||
| }, | ||
| "values": [ | ||
| { "$id": "4", "kind": "enumvalue", "name": "Dog", "value": "dog" }, | ||
| { "$id": "5", "kind": "enumvalue", "name": "Cat", "value": "cat" } | ||
| ], | ||
| "isFixed": true, | ||
| "usage": "Input,Output" | ||
| } | ||
| ], | ||
| "models": [ | ||
| { | ||
| "$id": "6", | ||
| "kind": "model", | ||
| "name": "Pet", | ||
| "crossLanguageDefinitionId": "PetStore.Pet", | ||
| "usage": "Input,Output", | ||
| "properties": [ | ||
| { | ||
| "$id": "7", | ||
| "kind": "property", | ||
| "name": "id", | ||
| "serializedName": "id", | ||
| "type": { "$id": "8", "kind": "int32", "name": "int32" }, | ||
| "optional": false | ||
| }, | ||
| { | ||
| "$id": "9", | ||
| "kind": "property", | ||
| "name": "name", | ||
| "serializedName": "name", | ||
| "type": { "$id": "10", "kind": "string", "name": "string" }, | ||
| "optional": false | ||
| }, | ||
| { | ||
| "$id": "11", | ||
| "kind": "property", | ||
| "name": "kind", | ||
| "serializedName": "kind", | ||
| "type": { "$ref": "2" }, | ||
| "optional": true | ||
| } | ||
| ] | ||
| } | ||
| ], | ||
| "clients": [ | ||
| { | ||
| "$id": "12", | ||
| "kind": "client", | ||
| "name": "PetStoreClient", | ||
| "namespace": "PetStore", | ||
| "operations": [ | ||
| { | ||
| "$id": "13", | ||
| "name": "getPet", | ||
| "resourceName": "Pet", | ||
| "verb": "get", | ||
| "path": "/pets/{petId}", | ||
| "responses": [{ "$id": "14", "statusCodes": [200], "bodyType": { "$ref": "6" } }] | ||
| } | ||
| ] | ||
| } | ||
| ] | ||
| } |
1 change: 1 addition & 0 deletions
1
...nt-csharp/playground-server.Tests/TestData/GenerationCacheTests/sample-configuration.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| { "package-name": "PetStore", "namespace": "PetStore", "library-name": "PetStore" } |
31 changes: 31 additions & 0 deletions
31
packages/http-client-csharp/playground-server.Tests/playground-server.Tests.csproj
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk"> | ||
|
|
||
| <PropertyGroup> | ||
| <TargetFramework>net10.0</TargetFramework> | ||
| <Nullable>enable</Nullable> | ||
| <ImplicitUsings>enable</ImplicitUsings> | ||
| <IsPackable>false</IsPackable> | ||
| <RootNamespace>PlaygroundServer.Tests</RootNamespace> | ||
| </PropertyGroup> | ||
|
|
||
| <ItemGroup> | ||
| <PackageReference Include="Microsoft.NET.Test.Sdk" /> | ||
| <PackageReference Include="NUnit" /> | ||
| <PackageReference Include="NUnit3TestAdapter" /> | ||
| <PackageReference Include="Microsoft.Extensions.Caching.Memory" /> | ||
| </ItemGroup> | ||
|
|
||
| <ItemGroup> | ||
| <ProjectReference Include="..\playground-server\playground-server.csproj" /> | ||
| </ItemGroup> | ||
|
|
||
| <ItemGroup> | ||
| <None Include="TestData\**\*"> | ||
| <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> | ||
| </None> | ||
| </ItemGroup> | ||
|
|
||
| <!-- Imported after the PackageReference items so the central Update versions apply to them. --> | ||
| <Import Project="..\generator\Packages.Data.props" /> | ||
|
|
||
| </Project> |
10 changes: 10 additions & 0 deletions
10
packages/http-client-csharp/playground-server/CachedGenerationResponse.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||
| // Licensed under the MIT License. | ||
|
|
||
| namespace PlaygroundServer; | ||
|
|
||
| /// <summary> | ||
| /// Cached generator response. Stored as the already-serialized JSON bytes plus | ||
| /// content type so cache hits can return without re-serializing. | ||
| /// </summary> | ||
| public sealed record CachedGenerationResponse(byte[] Body, string ContentType); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.