Skip to content

Commit 7ea6c66

Browse files
Imadnajambaywet
authored andcommitted
parallelize Descriptions setup, fix response leak, use typed stream keys (#3044)
- Dispose HttpResponseMessage in LoadFromUrlAsync (was leaking on every download) - Run the assembly reads and GHES downloads concurrently in GlobalSetup via Task.WhenAll instead of sequential awaits; aggregate into _streams afterward to avoid concurrent Dictionary writes - Replace string-keyed _streams (mixed file names / URLs) with a private DescriptionSource enum for compile-time-checked, pre-sized lookups - Drop the redundant async/await wrapper on each [Benchmark] method so the MemoryDiagnoser numbers reflect ParseDocumentAsync, not an extra state machine - Derive the four pinned GHES description URLs from one commit SHA constant instead of duplicating it across four literals - Add an HttpClient timeout; modernize Assembly access; align field naming
1 parent dc6c995 commit 7ea6c66

1 file changed

Lines changed: 81 additions & 57 deletions

File tree

performance/benchmark/Descriptions.cs

Lines changed: 81 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System;
1+
using System;
22
using System.Collections.Generic;
33
using System.IO;
44
using System.Net.Http;
@@ -15,88 +15,112 @@ namespace performance;
1515
[ShortRunJob]
1616
public class Descriptions
1717
{
18-
[Benchmark]
19-
public async Task<OpenApiDocument> PetStoreYaml()
18+
private enum DescriptionSource
2019
{
21-
return await ParseDocumentAsync(PetStoreYamlPath);
20+
PetStoreYaml,
21+
PetStoreJson,
22+
GHESYaml,
23+
GHESJson,
24+
GHESNextYaml,
25+
GHESNextJson
2226
}
27+
2328
[Benchmark]
24-
public async Task<OpenApiDocument> PetStoreJson()
25-
{
26-
return await ParseDocumentAsync(PetStoreJsonPath, OpenApiConstants.Json);
27-
}
29+
public Task<OpenApiDocument> PetStoreYaml() => ParseDocumentAsync(DescriptionSource.PetStoreYaml);
30+
2831
[Benchmark]
29-
public async Task<OpenApiDocument> GHESYaml()
30-
{
31-
return await ParseDocumentAsync(GHESYamlDescriptionUrl);
32-
}
32+
public Task<OpenApiDocument> PetStoreJson() => ParseDocumentAsync(DescriptionSource.PetStoreJson, OpenApiConstants.Json);
33+
3334
[Benchmark]
34-
public async Task<OpenApiDocument> GHESJson()
35-
{
36-
return await ParseDocumentAsync(GHESJsonDescriptionUrl, OpenApiConstants.Json);
37-
}
35+
public Task<OpenApiDocument> GHESYaml() => ParseDocumentAsync(DescriptionSource.GHESYaml);
36+
3837
[Benchmark]
39-
public async Task<OpenApiDocument> GHESNextYaml()
40-
{
41-
return await ParseDocumentAsync(GHESNextYamlDescriptionUrl);
42-
}
38+
public Task<OpenApiDocument> GHESJson() => ParseDocumentAsync(DescriptionSource.GHESJson, OpenApiConstants.Json);
39+
4340
[Benchmark]
44-
public async Task<OpenApiDocument> GHESNextJson()
45-
{
46-
return await ParseDocumentAsync(GHESNextJsonDescriptionUrl, OpenApiConstants.Json);
47-
}
48-
private readonly Dictionary<string, MemoryStream> _streams = new(StringComparer.OrdinalIgnoreCase);
41+
public Task<OpenApiDocument> GHESNextYaml() => ParseDocumentAsync(DescriptionSource.GHESNextYaml);
42+
43+
[Benchmark]
44+
public Task<OpenApiDocument> GHESNextJson() => ParseDocumentAsync(DescriptionSource.GHESNextJson, OpenApiConstants.Json);
45+
46+
private readonly Dictionary<DescriptionSource, MemoryStream> _streams = new(capacity: 6);
47+
4948
[GlobalSetup]
5049
public async Task GetAllDescriptions()
5150
{
52-
_httpClient = new HttpClient();
53-
readerSettings = new OpenApiReaderSettings
51+
_httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(60) };
52+
_readerSettings = new OpenApiReaderSettings { LeaveStreamOpen = true };
53+
_readerSettings.AddYamlReader();
54+
55+
var results = await Task.WhenAll(
56+
LoadFromAssemblyAsync(DescriptionSource.PetStoreYaml, PetStoreYamlResourceName),
57+
LoadFromAssemblyAsync(DescriptionSource.PetStoreJson, PetStoreJsonResourceName),
58+
LoadFromUrlAsync(DescriptionSource.GHESYaml, GHESYamlDescriptionUrl),
59+
LoadFromUrlAsync(DescriptionSource.GHESJson, GHESJsonDescriptionUrl),
60+
LoadFromUrlAsync(DescriptionSource.GHESNextYaml, GHESNextYamlDescriptionUrl),
61+
LoadFromUrlAsync(DescriptionSource.GHESNextJson, GHESNextJsonDescriptionUrl)
62+
).ConfigureAwait(false);
63+
64+
foreach (var (source, stream) in results)
5465
{
55-
LeaveStreamOpen = true,
56-
};
57-
readerSettings.AddYamlReader();
58-
await LoadDocumentFromAssemblyIntoStreams(PetStoreYamlPath);
59-
await LoadDocumentFromAssemblyIntoStreams(PetStoreJsonPath);
60-
await LoadDocumentFromUrlIntoStreams(GHESYamlDescriptionUrl);
61-
await LoadDocumentFromUrlIntoStreams(GHESJsonDescriptionUrl);
62-
await LoadDocumentFromUrlIntoStreams(GHESNextYamlDescriptionUrl);
63-
await LoadDocumentFromUrlIntoStreams(GHESNextJsonDescriptionUrl);
66+
_streams.Add(source, stream);
67+
}
6468
}
65-
private OpenApiReaderSettings readerSettings;
66-
private const string PetStoreYamlPath = @"petStore.yaml";
67-
private const string PetStoreJsonPath = @"petStore.json";
68-
private const string GHESYamlDescriptionUrl = @"https://raw.githubusercontent.com/github/rest-api-description/aef5e31a2d10fdaab311ec6d18a453021a81383d/descriptions/ghes-3.16/ghes-3.16.2022-11-28.yaml";
69-
private const string GHESJsonDescriptionUrl = @"https://raw.githubusercontent.com/github/rest-api-description/aef5e31a2d10fdaab311ec6d18a453021a81383d/descriptions/ghes-3.16/ghes-3.16.2022-11-28.json";
70-
private const string GHESNextYamlDescriptionUrl = @"https://raw.githubusercontent.com/github/rest-api-description/aef5e31a2d10fdaab311ec6d18a453021a81383d/descriptions-next/ghes-3.16/ghes-3.16.2022-11-28.yaml";
71-
private const string GHESNextJsonDescriptionUrl = @"https://raw.githubusercontent.com/github/rest-api-description/aef5e31a2d10fdaab311ec6d18a453021a81383d/descriptions-next/ghes-3.16/ghes-3.16.2022-11-28.json";
72-
private async Task<OpenApiDocument> ParseDocumentAsync(string fileName, string format = null)
69+
70+
private OpenApiReaderSettings _readerSettings;
71+
72+
private const string PetStoreYamlResourceName = "petStore.yaml";
73+
private const string PetStoreJsonResourceName = "petStore.json";
74+
75+
private const string GHESRepoCommitSha = "aef5e31a2d10fdaab311ec6d18a453021a81383d";
76+
private const string GHESReleaseVersion = "ghes-3.16";
77+
private const string GHESDescriptionFileName = "ghes-3.16.2022-11-28";
78+
79+
// Building the four GHES URLs from shared constants means the pinned commit only needs to
80+
// change in one place when the benchmark data set is refreshed, instead of four near-identical
81+
// literals that can silently drift out of sync with each other.
82+
private static string BuildGHESDescriptionUrl(string descriptionsFolder, string extension) =>
83+
$"https://raw.githubusercontent.com/github/rest-api-description/{GHESRepoCommitSha}/{descriptionsFolder}/{GHESReleaseVersion}/{GHESDescriptionFileName}.{extension}";
84+
85+
private static readonly string GHESYamlDescriptionUrl = BuildGHESDescriptionUrl("descriptions", "yaml");
86+
private static readonly string GHESJsonDescriptionUrl = BuildGHESDescriptionUrl("descriptions", "json");
87+
private static readonly string GHESNextYamlDescriptionUrl = BuildGHESDescriptionUrl("descriptions-next", "yaml");
88+
private static readonly string GHESNextJsonDescriptionUrl = BuildGHESDescriptionUrl("descriptions-next", "json");
89+
90+
private async Task<OpenApiDocument> ParseDocumentAsync(DescriptionSource source, string format = null)
7391
{
7492
format ??= OpenApiConstants.Yaml;
75-
var stream = _streams[fileName];
93+
var stream = _streams[source];
7694
stream.Seek(0, SeekOrigin.Begin);
77-
78-
var (document, _) = await OpenApiDocument.LoadAsync(stream, format, readerSettings).ConfigureAwait(false);
95+
96+
var (document, _) = await OpenApiDocument.LoadAsync(stream, format, _readerSettings).ConfigureAwait(false);
7997
return document;
8098
}
99+
81100
private HttpClient _httpClient;
82-
private async Task LoadDocumentFromUrlIntoStreams(string url)
101+
102+
private async Task<(DescriptionSource Source, MemoryStream Stream)> LoadFromUrlAsync(DescriptionSource source, string url)
83103
{
84-
var response = await _httpClient.GetAsync(url).ConfigureAwait(false);
104+
using var response = await _httpClient.GetAsync(url).ConfigureAwait(false);
85105
response.EnsureSuccessStatusCode();
86-
var stream = new MemoryStream(); // NOT disposed on purpose
106+
107+
var stream = new MemoryStream();
87108
await response.Content.CopyToAsync(stream).ConfigureAwait(false);
88109
stream.Seek(0, SeekOrigin.Begin);
89-
_streams.Add(url, stream);
110+
return (source, stream);
90111
}
91-
private static readonly Assembly assembly = typeof(Descriptions).GetTypeInfo().Assembly;
92-
private async Task LoadDocumentFromAssemblyIntoStreams(string fileName)
112+
113+
private static readonly Assembly _assembly = typeof(Descriptions).Assembly;
114+
115+
private async Task<(DescriptionSource Source, MemoryStream Stream)> LoadFromAssemblyAsync(DescriptionSource source, string resourceFileName)
93116
{
94-
using var resource = assembly.GetManifestResourceStream($"PerformanceTests.{fileName}");
95-
var stream = new MemoryStream(); // NOT disposed on purpose
96-
await resource.CopyToAsync(stream).ConfigureAwait(false);
117+
using var resourceStream = _assembly.GetManifestResourceStream($"PerformanceTests.{resourceFileName}");
118+
var stream = new MemoryStream();
119+
await resourceStream.CopyToAsync(stream).ConfigureAwait(false);
97120
stream.Seek(0, SeekOrigin.Begin);
98-
_streams.Add(fileName, stream);
121+
return (source, stream);
99122
}
123+
100124
[GlobalCleanup]
101125
public void Cleanup()
102126
{

0 commit comments

Comments
 (0)