From 8a53bfa5ac265c09ce7aaa41c9441123fad60a02 Mon Sep 17 00:00:00 2001 From: martincostello Date: Wed, 20 Aug 2025 10:03:58 +0100 Subject: [PATCH 1/9] Publish benchmark results Publish benchmark results to GitHub Actions workflow artifacts. --- .github/workflows/ci-cd.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 71f2c3cc1..06a1402cf 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -75,6 +75,13 @@ jobs: dotnet run -c Release working-directory: ./performance/benchmark + - name: Publish benchmark results + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + name: benchmark-results + path: "${{ github.workspace }}/performance/benchmark/BenchmarkDotNet.Artifacts/results" + - name: Run comparison tool for empty models run: dotnet run -c Release --project ./performance/resultsComparer/resultsComparer.csproj -- compare $OLD_REPORT $NEW_REPORT -p IdenticalMemoryUsage shell: bash From e6cb02f908b6b29727e6234b8818ed025037d9fa Mon Sep 17 00:00:00 2001 From: martincostello Date: Wed, 20 Aug 2025 10:04:34 +0100 Subject: [PATCH 2/9] Update .gitignore - Ignore BenchmarkDotNet profiler files. - Ignore Visual Studio profiler session files. --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index 4caae17f4..258fee87a 100644 --- a/.gitignore +++ b/.gitignore @@ -88,6 +88,7 @@ ipch/ *.VC.VC.opendb # Visual Studio profiler +*.diagsession *.psess *.vsp *.vspx @@ -286,3 +287,7 @@ __pycache__/ *.btm.cs *.odx.cs *.xsd.cs + +# BenchmarkDotNet profiler files +*.nettrace +*.speedscope.json From 84f657e6a446fde0c18c728bcbb37523cf890f1d Mon Sep 17 00:00:00 2001 From: martincostello Date: Wed, 20 Aug 2025 10:07:31 +0100 Subject: [PATCH 3/9] Improve OpenApiWalker performance - Avoid allocations from lambda closures. - Avoid allocations from context strings. - Avoid allocations from copying arrays. - Remove redundant null checks. --- .../Services/OpenApiWalker.cs | 653 +++++++++++------- 1 file changed, 387 insertions(+), 266 deletions(-) diff --git a/src/Microsoft.OpenApi/Services/OpenApiWalker.cs b/src/Microsoft.OpenApi/Services/OpenApiWalker.cs index 09a7e5a09..984dcff4a 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiWalker.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiWalker.cs @@ -42,14 +42,43 @@ public void Walk(OpenApiDocument? doc) _visitor.Visit(doc); - Walk(OpenApiConstants.Info, () => Walk(doc.Info)); - Walk(OpenApiConstants.Servers, () => Walk(doc.Servers)); - Walk(OpenApiConstants.Paths, () => Walk(doc.Paths)); - Walk(OpenApiConstants.Webhooks, () => Walk(doc.Webhooks)); - Walk(OpenApiConstants.Components, () => Walk(doc.Components)); - Walk(OpenApiConstants.Security, () => Walk(doc.Security)); - Walk(OpenApiConstants.ExternalDocs, () => Walk(doc.ExternalDocs)); - Walk(OpenApiConstants.Tags, () => Walk(doc.Tags)); + if (doc.Info is { } info) + { + WalkItem(OpenApiConstants.Info, doc.Info, Walk); + } + + if (doc.Servers is { } servers) + { + WalkItem(OpenApiConstants.Servers, servers, Walk); + } + + if (doc.Paths is { } paths) + { + WalkItem(OpenApiConstants.Paths, doc.Paths, Walk); + } + + WalkDictionary(OpenApiConstants.Webhooks, doc.Webhooks, Walk); + + if (doc.Components is { } components) + { + WalkItem(OpenApiConstants.Components, components, Walk); + } + + if (doc.Security is { } security) + { + WalkItem(OpenApiConstants.Security, security, Walk); + } + + if (doc.ExternalDocs is { } externalDocs) + { + WalkItem(OpenApiConstants.ExternalDocs, externalDocs, Walk); + } + + if (doc.Tags is { } tags) + { + WalkItem(OpenApiConstants.Tags, tags, Walk); + } + Walk(doc as IOpenApiExtensible); } @@ -66,12 +95,23 @@ internal void Walk(ISet? tags) _visitor.Visit(tags); // Visit tags - if (tags != null) + if (tags is HashSet { Count: 1 } hashSet && hashSet.First() is { } only) { - var tagsAsArray = tags.ToArray(); - for (var i = 0; i < tagsAsArray.Length; i++) + WalkItem("0", only, Walk); + } + else + { + int index = 0; + foreach (var tag in tags) { - Walk(i.ToString(), () => Walk(tagsAsArray[i])); + if (tag is null) + { + continue; + } + + var context = index.ToString(); + WalkItem(context, tag, Walk); + index++; } } } @@ -89,12 +129,23 @@ internal void Walk(ISet? tags) _visitor.Visit(tags); // Visit tags - if (tags != null) + if (tags is HashSet { Count: 1 } hashSet && hashSet.First() is { } only) + { + WalkItem("0", only, Walk); + } + else { - var referencesAsArray = tags.ToArray(); - for (var i = 0; i < referencesAsArray.Length; i++) + int index = 0; + foreach (var tag in tags) { - Walk(i.ToString(), () => Walk(referencesAsArray[i])); + if (tag is null) + { + continue; + } + + var context = index.ToString(); + WalkItem(context, tag, Walk); + index++; } } } @@ -136,115 +187,17 @@ internal void Walk(OpenApiComponents? components) _visitor.Visit(components); - Walk(OpenApiConstants.Schemas, () => - { - if (components.Schemas != null) - { - foreach (var item in components.Schemas) - { - Walk(item.Key, () => Walk(item.Value, isComponent: true)); - } - } - }); - - Walk(OpenApiConstants.SecuritySchemes, () => - { - if (components.SecuritySchemes != null) - { - foreach (var item in components.SecuritySchemes) - { - Walk(item.Key, () => Walk(item.Value, isComponent: true)); - } - } - }); - - Walk(OpenApiConstants.Callbacks, () => - { - if (components.Callbacks != null) - { - foreach (var item in components.Callbacks) - { - Walk(item.Key, () => Walk(item.Value, isComponent: true)); - } - } - }); - - Walk(OpenApiConstants.PathItems, () => - { - if (components.PathItems != null) - { - foreach (var path in components.PathItems) - { - Walk(path.Key, () => Walk(path.Value, isComponent: true)); - } - } - }); - - Walk(OpenApiConstants.Parameters, () => - { - if (components.Parameters != null) - { - foreach (var item in components.Parameters) - { - Walk(item.Key, () => Walk(item.Value, isComponent: true)); - } - } - }); - - Walk(OpenApiConstants.Examples, () => - { - if (components.Examples != null) - { - foreach (var item in components.Examples) - { - Walk(item.Key, () => Walk(item.Value, isComponent: true)); - } - } - }); - - Walk(OpenApiConstants.Headers, () => - { - if (components.Headers != null) - { - foreach (var item in components.Headers) - { - Walk(item.Key, () => Walk(item.Value, isComponent: true)); - } - } - }); - - Walk(OpenApiConstants.Links, () => - { - if (components.Links != null) - { - foreach (var item in components.Links) - { - Walk(item.Key, () => Walk(item.Value, isComponent: true)); - } - } - }); - - Walk(OpenApiConstants.RequestBodies, () => - { - if (components.RequestBodies != null) - { - foreach (var item in components.RequestBodies) - { - Walk(item.Key, () => Walk(item.Value, isComponent: true)); - } - } - }); - - Walk(OpenApiConstants.Responses, () => - { - if (components.Responses != null) - { - foreach (var item in components.Responses) - { - Walk(item.Key, () => Walk(item.Value, isComponent: true)); - } - } - }); + var isComponent = true; + WalkDictionary(OpenApiConstants.Schemas, components.Schemas, Walk, isComponent); + WalkDictionary(OpenApiConstants.SecuritySchemes, components.SecuritySchemes, Walk, isComponent); + WalkDictionary(OpenApiConstants.Callbacks, components.Callbacks, Walk, isComponent); + WalkDictionary(OpenApiConstants.PathItems, components.PathItems, Walk, isComponent); + WalkDictionary(OpenApiConstants.Parameters, components.Parameters, Walk, isComponent); + WalkDictionary(OpenApiConstants.Examples, components.Examples, Walk, isComponent); + WalkDictionary(OpenApiConstants.Headers, components.Headers, Walk, isComponent); + WalkDictionary(OpenApiConstants.Links, components.Links, Walk, isComponent); + WalkDictionary(OpenApiConstants.RequestBodies, components.RequestBodies, Walk, isComponent); + WalkDictionary(OpenApiConstants.Responses, components.Responses, Walk, isComponent); Walk(components as IOpenApiExtensible); } @@ -262,16 +215,17 @@ internal void Walk(OpenApiPaths paths) _visitor.Visit(paths); // Visit Paths - if (paths != null) + foreach (var pathItem in paths) { - foreach (var pathItem in paths) + if (pathItem.Value is null) { - _visitor.CurrentKeys.Path = pathItem.Key; - Walk(pathItem.Key, () => Walk(pathItem.Value));// JSON Pointer uses ~1 as an escape character for / - _visitor.CurrentKeys.Path = null; + continue; } - } + _visitor.CurrentKeys.Path = pathItem.Key; + WalkItem(pathItem.Key, pathItem.Value, Walk, isComponent: false);// JSON Pointer uses ~1 as an escape character for / + _visitor.CurrentKeys.Path = null; + } } /// @@ -287,14 +241,16 @@ internal void Walk(IDictionary? webhooks) _visitor.Visit(webhooks); // Visit Webhooks - if (webhooks != null) + foreach (var pathItem in webhooks) { - foreach (var pathItem in webhooks) + if (pathItem.Value is null) { - _visitor.CurrentKeys.Path = pathItem.Key; - Walk(pathItem.Key, () => Walk(pathItem.Value));// JSON Pointer uses ~1 as an escape character for / - _visitor.CurrentKeys.Path = null; + continue; } + + _visitor.CurrentKeys.Path = pathItem.Key; + WalkItem(pathItem.Key, pathItem.Value, Walk, isComponent: false);// JSON Pointer uses ~1 as an escape character for / + _visitor.CurrentKeys.Path = null; } } @@ -311,12 +267,14 @@ internal void Walk(IList? servers) _visitor.Visit(servers); // Visit Servers - if (servers != null) + for (var i = 0; i < servers.Count; i++) { - for (var i = 0; i < servers.Count; i++) + if (servers[i] is not { } server) { - Walk(i.ToString(), () => Walk(servers[i])); + continue; } + + WalkItem(i.ToString(), server, Walk); } } @@ -331,11 +289,17 @@ internal void Walk(OpenApiInfo info) } _visitor.Visit(info); - if (info != null) + + if (info.Contact is { } contact) + { + WalkItem(OpenApiConstants.Contact, contact, Walk); + } + + if (info.License is { } license) { - Walk(OpenApiConstants.Contact, () => Walk(info.Contact)); - Walk(OpenApiConstants.License, () => Walk(info.License)); + WalkItem(OpenApiConstants.License, license, Walk); } + Walk(info as IOpenApiExtensible); } @@ -355,8 +319,13 @@ internal void Walk(IOpenApiExtensible? openApiExtensible) { foreach (var item in openApiExtensible.Extensions) { + if (item.Value == null) + { + continue; + } + _visitor.CurrentKeys.Extension = item.Key; - Walk(item.Key, () => Walk(item.Value)); + WalkItem(item.Key, item.Value, Walk); _visitor.CurrentKeys.Extension = null; } } @@ -423,9 +392,15 @@ internal void Walk(IOpenApiCallback callback, bool isComponent = false) { foreach (var item in callback.PathItems) { - _visitor.CurrentKeys.Callback = item.Key.ToString(); - var pathItem = item.Value; - Walk(item.Key.ToString(), () => Walk(pathItem)); + if (item.Value is null) + { + continue; + } + + var context = item.Key.ToString(); + + _visitor.CurrentKeys.Callback = context; + WalkItem(context, item.Value, Walk); _visitor.CurrentKeys.Callback = null; } } @@ -442,9 +417,9 @@ internal void Walk(OpenApiTag tag) } _visitor.Visit(tag); - if (tag.ExternalDocs != null) + if (tag.ExternalDocs is { } externalDocs) { - _visitor.Visit(tag.ExternalDocs); + _visitor.Visit(externalDocs); } _visitor.Visit(tag as IOpenApiExtensible); } @@ -454,11 +429,6 @@ internal void Walk(OpenApiTag tag) /// internal void Walk(OpenApiTagReference tag) { - if (tag == null) - { - return; - } - if (tag is IOpenApiReferenceHolder openApiReferenceHolder) { Walk(openApiReferenceHolder); @@ -476,7 +446,12 @@ internal void Walk(OpenApiServer? server) } _visitor.Visit(server); - Walk(OpenApiConstants.Variables, () => Walk(server.Variables)); + + if (server.Variables is { } variables) + { + WalkItem(OpenApiConstants.Variables, variables, Walk); + } + _visitor.Visit(server as IOpenApiExtensible); } @@ -492,14 +467,16 @@ internal void Walk(IDictionary? serverVariables) _visitor.Visit(serverVariables); - if (serverVariables != null) + foreach (var variable in serverVariables) { - foreach (var variable in serverVariables) + if (variable.Value == null) { - _visitor.CurrentKeys.ServerVariable = variable.Key; - Walk(variable.Key, () => Walk(variable.Value)); - _visitor.CurrentKeys.ServerVariable = null; + continue; } + + _visitor.CurrentKeys.ServerVariable = variable.Key; + WalkItem(variable.Key, variable.Value, Walk); + _visitor.CurrentKeys.ServerVariable = null; } } @@ -546,7 +523,11 @@ internal void Walk(IOpenApiPathItem pathItem, bool isComponent = false) if (pathItem != null) { - Walk(OpenApiConstants.Parameters, () => Walk(pathItem.Parameters)); + if (pathItem.Parameters is { } parameters) + { + WalkItem(OpenApiConstants.Parameters, parameters, Walk); + } + Walk(pathItem.Operations); } @@ -568,14 +549,17 @@ internal void Walk(IDictionary? operations) } _visitor.Visit(operations); - if (operations != null) + + foreach (var operation in operations) { - foreach (var operation in operations) + if (operation.Value is null) { - _visitor.CurrentKeys.Operation = operation.Key; - Walk(operation.Key.Method.ToLowerInvariant(), () => Walk(operation.Value)); - _visitor.CurrentKeys.Operation = null; + continue; } + + _visitor.CurrentKeys.Operation = operation.Key; + WalkItem(operation.Key.Method.ToLowerInvariant(), operation.Value, Walk); + _visitor.CurrentKeys.Operation = null; } } @@ -592,12 +576,30 @@ internal void Walk(OpenApiOperation operation) _visitor.Visit(operation); - Walk(OpenApiConstants.Parameters, () => Walk(operation.Parameters)); + if (operation.Parameters is { } parameters) + { + WalkItem(OpenApiConstants.Parameters, parameters, Walk); + } + Walk(OpenApiConstants.RequestBody, () => Walk(operation.RequestBody)); - Walk(OpenApiConstants.Responses, () => Walk(operation.Responses)); - Walk(OpenApiConstants.Callbacks, () => Walk(operation.Callbacks)); - Walk(OpenApiConstants.Tags, () => Walk(operation.Tags)); - Walk(OpenApiConstants.Security, () => Walk(operation.Security)); + + if (operation.Responses is { } responses) + { + WalkItem(OpenApiConstants.Responses, responses, Walk); + } + + WalkDictionary(OpenApiConstants.Callbacks, operation.Callbacks, Walk); + + if (operation.Tags is { } tags) + { + WalkItem(OpenApiConstants.Tags, tags, Walk); + } + + if (operation.Security is { } security) + { + WalkItem(OpenApiConstants.Security, security, Walk); + } + Walk(operation as IOpenApiExtensible); } @@ -613,12 +615,14 @@ internal void Walk(IList? securityRequirements) _visitor.Visit(securityRequirements); - if (securityRequirements != null) + for (var i = 0; i < securityRequirements.Count; i++) { - for (var i = 0; i < securityRequirements.Count; i++) + if (securityRequirements[i] is not { } requirement) { - Walk(i.ToString(), () => Walk(securityRequirements[i])); + continue; } + + WalkItem(i.ToString(), requirement, Walk); } } @@ -634,12 +638,14 @@ internal void Walk(IList? parameters) _visitor.Visit(parameters); - if (parameters != null) + for (var i = 0; i < parameters.Count; i++) { - for (var i = 0; i < parameters.Count; i++) + if (parameters[i] is not { } parameter) { - Walk(i.ToString(), () => Walk(parameters[i])); + continue; } + + WalkItem(i.ToString(), parameter, Walk); } } @@ -660,9 +666,18 @@ internal void Walk(IOpenApiParameter parameter, bool isComponent = false) } _visitor.Visit(parameter); - Walk(OpenApiConstants.Schema, () => Walk(parameter.Schema)); - Walk(OpenApiConstants.Content, () => Walk(parameter.Content)); - Walk(OpenApiConstants.Examples, () => Walk(parameter.Examples)); + + if (parameter.Schema is { } schema) + { + WalkItem(OpenApiConstants.Schema, schema, Walk, isComponent: false); + } + + if (parameter.Content is { } content) + { + WalkItem(OpenApiConstants.Content, content, Walk); + } + + WalkDictionary(OpenApiConstants.Examples, parameter.Examples, Walk); Walk(parameter as IOpenApiExtensible); } @@ -679,15 +694,18 @@ internal void Walk(OpenApiResponses? responses) _visitor.Visit(responses); - if (responses != null) + foreach (var response in responses) { - foreach (var response in responses) + if (response.Value is null) { - _visitor.CurrentKeys.Response = response.Key; - Walk(response.Key, () => Walk(response.Value)); - _visitor.CurrentKeys.Response = null; + continue; } + + _visitor.CurrentKeys.Response = response.Key; + WalkItem(response.Key, response.Value, Walk, isComponent: false); + _visitor.CurrentKeys.Response = null; } + Walk(responses as IOpenApiExtensible); } @@ -708,9 +726,14 @@ internal void Walk(IOpenApiResponse response, bool isComponent = false) } _visitor.Visit(response); - Walk(OpenApiConstants.Content, () => Walk(response.Content)); - Walk(OpenApiConstants.Links, () => Walk(response.Links)); - Walk(OpenApiConstants.Headers, () => Walk(response.Headers)); + + if (response.Content is { } content) + { + WalkItem(OpenApiConstants.Content, content, Walk); + } + + WalkDictionary(OpenApiConstants.Links, response.Links, Walk); + WalkDictionary(OpenApiConstants.Headers, response.Headers, Walk); Walk(response as IOpenApiExtensible); } @@ -732,10 +755,11 @@ internal void Walk(IOpenApiRequestBody? requestBody, bool isComponent = false) _visitor.Visit(requestBody); - if (requestBody is {Content: not null}) + if (requestBody.Content is { } content) { - Walk(OpenApiConstants.Content, () => Walk(requestBody.Content)); + WalkItem(OpenApiConstants.Content, content, Walk); } + Walk(requestBody as IOpenApiExtensible); } @@ -750,14 +774,17 @@ internal void Walk(IDictionary? headers) } _visitor.Visit(headers); - if (headers != null) + + foreach (var header in headers) { - foreach (var header in headers) + if (header.Value is null) { - _visitor.CurrentKeys.Header = header.Key; - Walk(header.Key, () => Walk(header.Value)); - _visitor.CurrentKeys.Header = null; + continue; } + + _visitor.CurrentKeys.Header = header.Key; + WalkItem(header.Key, header.Value, Walk, isComponent: false); + _visitor.CurrentKeys.Header = null; } } @@ -772,14 +799,17 @@ internal void Walk(IDictionary? callbacks) } _visitor.Visit(callbacks); - if (callbacks != null) + + foreach (var callback in callbacks) { - foreach (var callback in callbacks) + if (callback.Value is null) { - _visitor.CurrentKeys.Callback = callback.Key; - Walk(callback.Key, () => Walk(callback.Value)); - _visitor.CurrentKeys.Callback = null; + continue; } + + _visitor.CurrentKeys.Callback = callback.Key; + WalkItem(callback.Key, callback.Value, Walk, isComponent: false); + _visitor.CurrentKeys.Callback = null; } } @@ -794,14 +824,17 @@ internal void Walk(IDictionary? content) } _visitor.Visit(content); - if (content != null) + + foreach (var mediaType in content) { - foreach (var mediaType in content) + if (mediaType.Value is null) { - _visitor.CurrentKeys.Content = mediaType.Key; - Walk(mediaType.Key, () => Walk(mediaType.Value)); - _visitor.CurrentKeys.Content = null; + continue; } + + _visitor.CurrentKeys.Content = mediaType.Key; + WalkItem(mediaType.Key, mediaType.Value, Walk); + _visitor.CurrentKeys.Content = null; } } @@ -817,9 +850,18 @@ internal void Walk(OpenApiMediaType mediaType) _visitor.Visit(mediaType); - Walk(OpenApiConstants.Example, () => Walk(mediaType.Examples)); - Walk(OpenApiConstants.Schema, () => Walk(mediaType.Schema)); - Walk(OpenApiConstants.Encoding, () => Walk(mediaType.Encoding)); + WalkDictionary(OpenApiConstants.Example, mediaType.Examples, Walk); + + if (mediaType.Schema is { } schema) + { + WalkItem(OpenApiConstants.Schema, schema, Walk, isComponent: false); + } + + if (mediaType.Encoding is { } encoding) + { + WalkItem(OpenApiConstants.Encoding, encoding, Walk); + } + Walk(mediaType as IOpenApiExtensible); } @@ -835,14 +877,16 @@ internal void Walk(IDictionary? encodings) _visitor.Visit(encodings); - if (encodings != null) + foreach (var item in encodings) { - foreach (var item in encodings) + if (item.Value is null) { - _visitor.CurrentKeys.Encoding = item.Key; - Walk(item.Key, () => Walk(item.Value)); - _visitor.CurrentKeys.Encoding = null; + continue; } + + _visitor.CurrentKeys.Encoding = item.Key; + WalkItem(item.Key, item.Value, Walk); + _visitor.CurrentKeys.Encoding = null; } } @@ -858,10 +902,11 @@ internal void Walk(OpenApiEncoding encoding) _visitor.Visit(encoding); - if (encoding.Headers != null) + if (encoding.Headers is { } headers) { - Walk(encoding.Headers); + Walk(headers); } + Walk(encoding as IOpenApiExtensible); } @@ -886,50 +931,50 @@ internal void Walk(IOpenApiSchema? schema, bool isComponent = false) _visitor.Visit(schema); - if (schema.Items != null) + if (schema.Items is { } items) { - Walk("items", () => Walk(schema.Items)); + WalkItem("items", items, Walk, isComponent: false); } - if (schema.Not != null) + if (schema.Not is { } not) { - Walk("not", () => Walk(schema.Not)); + WalkItem("not", not, Walk, isComponent: false); } - if (schema.AllOf != null) + if (schema.AllOf is { Count: > 0 } allOf) { - Walk("allOf", () => Walk(schema.AllOf)); + WalkItem("allOf", allOf, Walk); } - if (schema.AnyOf != null) + if (schema.AnyOf is { Count: > 0 } anyOf) { - Walk("anyOf", () => Walk(schema.AnyOf)); + WalkItem("anyOf", anyOf, Walk); } - if (schema.OneOf != null) + if (schema.OneOf is { Count: > 0 } oneOf) { - Walk("oneOf", () => Walk(schema.OneOf)); + WalkItem("oneOf", oneOf, Walk); } - if (schema.Properties != null) + if (schema.Properties is { } properties) { - Walk("properties", () => - { - foreach (var item in schema.Properties) - { - Walk(item.Key, () => Walk(item.Value)); - } - }); + WalkDictionary("properties", properties, Walk); } - if (schema.AdditionalProperties != null) + if (schema.AdditionalProperties is { } additionalProperties) { - Walk("additionalProperties", () => Walk(schema.AdditionalProperties)); + WalkItem("additionalProperties", additionalProperties, Walk, isComponent: false); } - Walk("discriminator", () => Walk(schema.Discriminator)); + if (schema.Discriminator is { } discriminator) + { + WalkItem("discriminator", discriminator, Walk); + } - Walk(OpenApiConstants.ExternalDocs, () => Walk(schema.ExternalDocs)); + if (schema.ExternalDocs is { } externalDocs) + { + WalkItem(OpenApiConstants.ExternalDocs, externalDocs, Walk); + } Walk(schema as IOpenApiExtensible); @@ -945,19 +990,12 @@ internal void Walk(OpenApiDiscriminator? openApiDiscriminator) _visitor.Visit(openApiDiscriminator); - if (openApiDiscriminator.Mapping != null) + if (openApiDiscriminator.Mapping is { Count: > 0 } mapping) { - Walk("mapping", () => - { - foreach (var item in openApiDiscriminator.Mapping) - { - Walk(item.Key, () => Walk((IOpenApiSchema)item.Value)); - } - }); + WalkDictionary("mapping", mapping, (item, _) => Walk((IOpenApiSchema)item)); } } - /// /// Visits dictionary of /// @@ -970,14 +1008,16 @@ internal void Walk(IDictionary? examples) _visitor.Visit(examples); - if (examples != null) + foreach (var example in examples) { - foreach (var example in examples) + if (example.Value is null) { - _visitor.CurrentKeys.Example = example.Key; - Walk(example.Key, () => Walk(example.Value)); - _visitor.CurrentKeys.Example = null; + continue; } + + _visitor.CurrentKeys.Example = example.Key; + WalkItem(example.Key, example.Value, Walk, isComponent: false); + _visitor.CurrentKeys.Example = null; } } @@ -1027,12 +1067,14 @@ internal void Walk(List examples) _visitor.Visit(examples); // Visit Examples - if (examples != null) + for (var i = 0; i < examples.Count; i++) { - for (var i = 0; i < examples.Count; i++) + if (examples[i] is not { } example) { - Walk(i.ToString(), () => Walk(examples[i])); + continue; } + + WalkItem(i.ToString(), example, Walk, isComponent: false); } } @@ -1047,12 +1089,14 @@ internal void Walk(IList schemas) } // Visit Schemas - if (schemas != null) + for (var i = 0; i < schemas.Count; i++) { - for (var i = 0; i < schemas.Count; i++) + if (schemas[i] is not { } schema) { - Walk(i.ToString(), () => Walk(schemas[i])); + continue; } + + WalkItem(i.ToString(), schema, Walk, isComponent: false); } } @@ -1095,14 +1139,16 @@ internal void Walk(IDictionary? links) _visitor.Visit(links); - if (links != null) + foreach (var item in links) { - foreach (var item in links) + if (item.Value is null) { - _visitor.CurrentKeys.Link = item.Key; - Walk(item.Key, () => Walk(item.Value)); - _visitor.CurrentKeys.Link = null; + continue; } + + _visitor.CurrentKeys.Link = item.Key; + WalkItem(item.Key, item.Value, Walk, isComponent: false); + _visitor.CurrentKeys.Link = null; } } @@ -1123,7 +1169,12 @@ internal void Walk(IOpenApiLink link, bool isComponent = false) } _visitor.Visit(link); - Walk(OpenApiConstants.Server, () => Walk(link.Server)); + + if (link.Server is { } server) + { + WalkItem(OpenApiConstants.Server, server, Walk); + } + Walk(link as IOpenApiExtensible); } @@ -1144,10 +1195,24 @@ internal void Walk(IOpenApiHeader header, bool isComponent = false) } _visitor.Visit(header); - Walk(OpenApiConstants.Content, () => Walk(header.Content)); - Walk(OpenApiConstants.Example, () => Walk(header.Example)); - Walk(OpenApiConstants.Examples, () => Walk(header.Examples)); - Walk(OpenApiConstants.Schema, () => Walk(header.Schema)); + + if (header.Content is { } content) + { + WalkItem(OpenApiConstants.Content, content, Walk); + } + + if (header.Example is { } example) + { + WalkItem(OpenApiConstants.Example, example, Walk); + } + + WalkDictionary(OpenApiConstants.Examples, header.Examples, Walk); + + if (header.Schema is { } schema) + { + WalkItem(OpenApiConstants.Schema, schema, Walk, isComponent: false); + } + Walk(header as IOpenApiExtensible); } @@ -1257,6 +1322,62 @@ private void Walk(string context, Action walk) _visitor.Exit(); } + /// + /// Adds a segment to the context path to enable pointing to the current location in the document + /// + /// The type of the state. + /// An identifier for the context. + /// The state to pass to the walk action. + /// An action that walks objects within the context. + private void WalkItem(string context, T state, Action walk) + { + _visitor.Enter(context.Replace("/", "~1")); + walk(state); + _visitor.Exit(); + } + + /// + /// Adds a segment to the context path to enable pointing to the current location in the document + /// + /// The type of the state. + /// An identifier for the context. + /// The state to pass to the walk action. + /// Whether the state is a component. + /// An action that walks objects within the context. + private void WalkItem(string context, T state, Action walk, bool isComponent) + { + _visitor.Enter(context.Replace("/", "~1")); + walk(state, isComponent); + _visitor.Exit(); + } + + /// + /// Adds a segment to the context path to enable pointing to the current location in the document + /// + /// The type of the state. + /// An identifier for the context. + /// The state to pass to the walk action. + /// Whether the state is a component. + /// An action that walks objects within the context. + private void WalkDictionary( + string context, + IDictionary? state, + Action walk, + bool isComponent = false) + { + if (state != null && state.Count > 0) + { + _visitor.Enter(context.Replace("/", "~1")); + + foreach (var item in state) + { + WalkItem(item.Key, (item.Value, isComponent), (state) => walk(state.Value, state.isComponent)); + } + + _visitor.Exit(); + } + } + /// /// Identify if an element is just a reference to a component, or an actual component /// From 4b8ee8a4d9229f9342c0d6e9b3eb9f9a19c5eefa Mon Sep 17 00:00:00 2001 From: martincostello Date: Wed, 20 Aug 2025 12:59:58 +0100 Subject: [PATCH 4/9] Reduce allocations further Make all delegates static. --- .../Services/OpenApiWalker.cs | 165 +++++++++--------- 1 file changed, 84 insertions(+), 81 deletions(-) diff --git a/src/Microsoft.OpenApi/Services/OpenApiWalker.cs b/src/Microsoft.OpenApi/Services/OpenApiWalker.cs index 984dcff4a..e1603be8b 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiWalker.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiWalker.cs @@ -44,39 +44,39 @@ public void Walk(OpenApiDocument? doc) if (doc.Info is { } info) { - WalkItem(OpenApiConstants.Info, doc.Info, Walk); + WalkItem(OpenApiConstants.Info, doc.Info, static (self, item) => self.Walk(item)); } if (doc.Servers is { } servers) { - WalkItem(OpenApiConstants.Servers, servers, Walk); + WalkItem(OpenApiConstants.Servers, servers, static (self, item) => self.Walk(item)); } if (doc.Paths is { } paths) { - WalkItem(OpenApiConstants.Paths, doc.Paths, Walk); + WalkItem(OpenApiConstants.Paths, doc.Paths, static (self, item) => self.Walk(item)); } - WalkDictionary(OpenApiConstants.Webhooks, doc.Webhooks, Walk); + WalkDictionary(OpenApiConstants.Webhooks, doc.Webhooks, static (self, item, isComponent) => self.Walk(item, isComponent)); if (doc.Components is { } components) { - WalkItem(OpenApiConstants.Components, components, Walk); + WalkItem(OpenApiConstants.Components, components, static (self, item) => self.Walk(item)); } if (doc.Security is { } security) { - WalkItem(OpenApiConstants.Security, security, Walk); + WalkItem(OpenApiConstants.Security, security, static (self, item) => self.Walk(item)); } if (doc.ExternalDocs is { } externalDocs) { - WalkItem(OpenApiConstants.ExternalDocs, externalDocs, Walk); + WalkItem(OpenApiConstants.ExternalDocs, externalDocs, static (self, item) => self.Walk(item)); } if (doc.Tags is { } tags) { - WalkItem(OpenApiConstants.Tags, tags, Walk); + WalkItem(OpenApiConstants.Tags, tags, static (self, item) => self.Walk(item)); } Walk(doc as IOpenApiExtensible); @@ -97,7 +97,7 @@ internal void Walk(ISet? tags) // Visit tags if (tags is HashSet { Count: 1 } hashSet && hashSet.First() is { } only) { - WalkItem("0", only, Walk); + WalkItem("0", only, static (self, item) => self.Walk(item)); } else { @@ -110,7 +110,7 @@ internal void Walk(ISet? tags) } var context = index.ToString(); - WalkItem(context, tag, Walk); + WalkItem(context, tag, static (self, item) => self.Walk(item)); index++; } } @@ -131,7 +131,7 @@ internal void Walk(ISet? tags) // Visit tags if (tags is HashSet { Count: 1 } hashSet && hashSet.First() is { } only) { - WalkItem("0", only, Walk); + WalkItem("0", only, static (self, item) => self.Walk(item)); } else { @@ -144,7 +144,7 @@ internal void Walk(ISet? tags) } var context = index.ToString(); - WalkItem(context, tag, Walk); + WalkItem(context, tag, static (self, item) => self.Walk(item)); index++; } } @@ -188,16 +188,16 @@ internal void Walk(OpenApiComponents? components) _visitor.Visit(components); var isComponent = true; - WalkDictionary(OpenApiConstants.Schemas, components.Schemas, Walk, isComponent); - WalkDictionary(OpenApiConstants.SecuritySchemes, components.SecuritySchemes, Walk, isComponent); - WalkDictionary(OpenApiConstants.Callbacks, components.Callbacks, Walk, isComponent); - WalkDictionary(OpenApiConstants.PathItems, components.PathItems, Walk, isComponent); - WalkDictionary(OpenApiConstants.Parameters, components.Parameters, Walk, isComponent); - WalkDictionary(OpenApiConstants.Examples, components.Examples, Walk, isComponent); - WalkDictionary(OpenApiConstants.Headers, components.Headers, Walk, isComponent); - WalkDictionary(OpenApiConstants.Links, components.Links, Walk, isComponent); - WalkDictionary(OpenApiConstants.RequestBodies, components.RequestBodies, Walk, isComponent); - WalkDictionary(OpenApiConstants.Responses, components.Responses, Walk, isComponent); + WalkDictionary(OpenApiConstants.Schemas, components.Schemas, static (self, item, isComponent) => self.Walk(item), isComponent); + WalkDictionary(OpenApiConstants.SecuritySchemes, components.SecuritySchemes, static (self, item, isComponent) => self.Walk(item), isComponent); + WalkDictionary(OpenApiConstants.Callbacks, components.Callbacks, static (self, item, isComponent) => self.Walk(item), isComponent); + WalkDictionary(OpenApiConstants.PathItems, components.PathItems, static (self, item, isComponent) => self.Walk(item), isComponent); + WalkDictionary(OpenApiConstants.Parameters, components.Parameters, static (self, item, isComponent) => self.Walk(item), isComponent); + WalkDictionary(OpenApiConstants.Examples, components.Examples, static (self, item, isComponent) => self.Walk(item), isComponent); + WalkDictionary(OpenApiConstants.Headers, components.Headers, static (self, item, isComponent) => self.Walk(item), isComponent); + WalkDictionary(OpenApiConstants.Links, components.Links, static (self, item, isComponent) => self.Walk(item), isComponent); + WalkDictionary(OpenApiConstants.RequestBodies, components.RequestBodies, static (self, item, isComponent) => self.Walk(item), isComponent); + WalkDictionary(OpenApiConstants.Responses, components.Responses, static (self, item, isComponent) => self.Walk(item), isComponent); Walk(components as IOpenApiExtensible); } @@ -223,7 +223,7 @@ internal void Walk(OpenApiPaths paths) } _visitor.CurrentKeys.Path = pathItem.Key; - WalkItem(pathItem.Key, pathItem.Value, Walk, isComponent: false);// JSON Pointer uses ~1 as an escape character for / + WalkItem(pathItem.Key, pathItem.Value, static (self, item, isComponent) => self.Walk(item, isComponent), isComponent: false);// JSON Pointer uses ~1 as an escape character for / _visitor.CurrentKeys.Path = null; } } @@ -249,7 +249,7 @@ internal void Walk(IDictionary? webhooks) } _visitor.CurrentKeys.Path = pathItem.Key; - WalkItem(pathItem.Key, pathItem.Value, Walk, isComponent: false);// JSON Pointer uses ~1 as an escape character for / + WalkItem(pathItem.Key, pathItem.Value, static (self, item, isComponent) => self.Walk(item, isComponent), isComponent: false);// JSON Pointer uses ~1 as an escape character for / _visitor.CurrentKeys.Path = null; } } @@ -274,7 +274,7 @@ internal void Walk(IList? servers) continue; } - WalkItem(i.ToString(), server, Walk); + WalkItem(i.ToString(), server, static (self, item) => self.Walk(item)); } } @@ -292,12 +292,12 @@ internal void Walk(OpenApiInfo info) if (info.Contact is { } contact) { - WalkItem(OpenApiConstants.Contact, contact, Walk); + WalkItem(OpenApiConstants.Contact, contact, static (self, item) => self.Walk(item)); } if (info.License is { } license) { - WalkItem(OpenApiConstants.License, license, Walk); + WalkItem(OpenApiConstants.License, license, static (self, item) => self.Walk(item)); } Walk(info as IOpenApiExtensible); @@ -325,7 +325,7 @@ internal void Walk(IOpenApiExtensible? openApiExtensible) } _visitor.CurrentKeys.Extension = item.Key; - WalkItem(item.Key, item.Value, Walk); + WalkItem(item.Key, item.Value, static (self, item) => self.Walk(item)); _visitor.CurrentKeys.Extension = null; } } @@ -400,7 +400,7 @@ internal void Walk(IOpenApiCallback callback, bool isComponent = false) var context = item.Key.ToString(); _visitor.CurrentKeys.Callback = context; - WalkItem(context, item.Value, Walk); + WalkItem(context, item.Value, static (self, item) => self.Walk(item)); _visitor.CurrentKeys.Callback = null; } } @@ -449,7 +449,7 @@ internal void Walk(OpenApiServer? server) if (server.Variables is { } variables) { - WalkItem(OpenApiConstants.Variables, variables, Walk); + WalkItem(OpenApiConstants.Variables, variables, static (self, item) => self.Walk(item)); } _visitor.Visit(server as IOpenApiExtensible); @@ -475,7 +475,7 @@ internal void Walk(IDictionary? serverVariables) } _visitor.CurrentKeys.ServerVariable = variable.Key; - WalkItem(variable.Key, variable.Value, Walk); + WalkItem(variable.Key, variable.Value, static (self, item) => self.Walk(item)); _visitor.CurrentKeys.ServerVariable = null; } } @@ -525,7 +525,7 @@ internal void Walk(IOpenApiPathItem pathItem, bool isComponent = false) { if (pathItem.Parameters is { } parameters) { - WalkItem(OpenApiConstants.Parameters, parameters, Walk); + WalkItem(OpenApiConstants.Parameters, parameters, static (self, item) => self.Walk(item)); } Walk(pathItem.Operations); @@ -558,7 +558,7 @@ internal void Walk(IDictionary? operations) } _visitor.CurrentKeys.Operation = operation.Key; - WalkItem(operation.Key.Method.ToLowerInvariant(), operation.Value, Walk); + WalkItem(operation.Key.Method.ToLowerInvariant(), operation.Value, static (self, item) => self.Walk(item)); _visitor.CurrentKeys.Operation = null; } } @@ -578,26 +578,29 @@ internal void Walk(OpenApiOperation operation) if (operation.Parameters is { } parameters) { - WalkItem(OpenApiConstants.Parameters, parameters, Walk); + WalkItem(OpenApiConstants.Parameters, parameters, static (self, item) => self.Walk(item)); } - Walk(OpenApiConstants.RequestBody, () => Walk(operation.RequestBody)); + if (operation.RequestBody is { } requestBody) + { + WalkItem(OpenApiConstants.RequestBody, requestBody , static (self, item) => self.Walk(item)); + } if (operation.Responses is { } responses) { - WalkItem(OpenApiConstants.Responses, responses, Walk); + WalkItem(OpenApiConstants.Responses, responses, static (self, item) => self.Walk(item)); } - WalkDictionary(OpenApiConstants.Callbacks, operation.Callbacks, Walk); + WalkDictionary(OpenApiConstants.Callbacks, operation.Callbacks, static (self, item, isComponent) => self.Walk(item, isComponent)); if (operation.Tags is { } tags) { - WalkItem(OpenApiConstants.Tags, tags, Walk); + WalkItem(OpenApiConstants.Tags, tags, static (self, item) => self.Walk(item)); } if (operation.Security is { } security) { - WalkItem(OpenApiConstants.Security, security, Walk); + WalkItem(OpenApiConstants.Security, security, static (self, item) => self.Walk(item)); } Walk(operation as IOpenApiExtensible); @@ -622,7 +625,7 @@ internal void Walk(IList? securityRequirements) continue; } - WalkItem(i.ToString(), requirement, Walk); + WalkItem(i.ToString(), requirement, static (self, item) => self.Walk(item)); } } @@ -645,7 +648,7 @@ internal void Walk(IList? parameters) continue; } - WalkItem(i.ToString(), parameter, Walk); + WalkItem(i.ToString(), parameter, static (self, item) => self.Walk(item)); } } @@ -669,15 +672,15 @@ internal void Walk(IOpenApiParameter parameter, bool isComponent = false) if (parameter.Schema is { } schema) { - WalkItem(OpenApiConstants.Schema, schema, Walk, isComponent: false); + WalkItem(OpenApiConstants.Schema, schema, static (self, item, isComponent) => self.Walk(item, isComponent), isComponent: false); } if (parameter.Content is { } content) { - WalkItem(OpenApiConstants.Content, content, Walk); + WalkItem(OpenApiConstants.Content, content, static (self, item) => self.Walk(item)); } - WalkDictionary(OpenApiConstants.Examples, parameter.Examples, Walk); + WalkDictionary(OpenApiConstants.Examples, parameter.Examples, static (self, item, isComponent) => self.Walk(item, isComponent)); Walk(parameter as IOpenApiExtensible); } @@ -702,7 +705,7 @@ internal void Walk(OpenApiResponses? responses) } _visitor.CurrentKeys.Response = response.Key; - WalkItem(response.Key, response.Value, Walk, isComponent: false); + WalkItem(response.Key, response.Value, static (self, item, isComponent) => self.Walk(item, isComponent), isComponent: false); _visitor.CurrentKeys.Response = null; } @@ -729,11 +732,11 @@ internal void Walk(IOpenApiResponse response, bool isComponent = false) if (response.Content is { } content) { - WalkItem(OpenApiConstants.Content, content, Walk); + WalkItem(OpenApiConstants.Content, content, static (self, item) => self.Walk(item)); } - WalkDictionary(OpenApiConstants.Links, response.Links, Walk); - WalkDictionary(OpenApiConstants.Headers, response.Headers, Walk); + WalkDictionary(OpenApiConstants.Links, response.Links, static (self, item, isComponent) => self.Walk(item, isComponent)); + WalkDictionary(OpenApiConstants.Headers, response.Headers, static (self, item, isComponent) => self.Walk(item, isComponent)); Walk(response as IOpenApiExtensible); } @@ -757,7 +760,7 @@ internal void Walk(IOpenApiRequestBody? requestBody, bool isComponent = false) if (requestBody.Content is { } content) { - WalkItem(OpenApiConstants.Content, content, Walk); + WalkItem(OpenApiConstants.Content, content, static (self, item) => self.Walk(item)); } Walk(requestBody as IOpenApiExtensible); @@ -783,7 +786,7 @@ internal void Walk(IDictionary? headers) } _visitor.CurrentKeys.Header = header.Key; - WalkItem(header.Key, header.Value, Walk, isComponent: false); + WalkItem(header.Key, header.Value, static (self, item, isComponent) => self.Walk(item, isComponent), isComponent: false); _visitor.CurrentKeys.Header = null; } } @@ -808,7 +811,7 @@ internal void Walk(IDictionary? callbacks) } _visitor.CurrentKeys.Callback = callback.Key; - WalkItem(callback.Key, callback.Value, Walk, isComponent: false); + WalkItem(callback.Key, callback.Value, static (self, item, isComponent) => self.Walk(item, isComponent), isComponent: false); _visitor.CurrentKeys.Callback = null; } } @@ -833,7 +836,7 @@ internal void Walk(IDictionary? content) } _visitor.CurrentKeys.Content = mediaType.Key; - WalkItem(mediaType.Key, mediaType.Value, Walk); + WalkItem(mediaType.Key, mediaType.Value, static (self, item) => self.Walk(item)); _visitor.CurrentKeys.Content = null; } } @@ -850,16 +853,16 @@ internal void Walk(OpenApiMediaType mediaType) _visitor.Visit(mediaType); - WalkDictionary(OpenApiConstants.Example, mediaType.Examples, Walk); + WalkDictionary(OpenApiConstants.Example, mediaType.Examples, static (self, item, isComponent) => self.Walk(item, isComponent)); if (mediaType.Schema is { } schema) { - WalkItem(OpenApiConstants.Schema, schema, Walk, isComponent: false); + WalkItem(OpenApiConstants.Schema, schema, static (self, item, isComponent) => self.Walk(item, isComponent), isComponent: false); } if (mediaType.Encoding is { } encoding) { - WalkItem(OpenApiConstants.Encoding, encoding, Walk); + WalkItem(OpenApiConstants.Encoding, encoding, static (self, item) => self.Walk(item)); } Walk(mediaType as IOpenApiExtensible); @@ -885,7 +888,7 @@ internal void Walk(IDictionary? encodings) } _visitor.CurrentKeys.Encoding = item.Key; - WalkItem(item.Key, item.Value, Walk); + WalkItem(item.Key, item.Value, static (self, item) => self.Walk(item)); _visitor.CurrentKeys.Encoding = null; } } @@ -933,47 +936,47 @@ internal void Walk(IOpenApiSchema? schema, bool isComponent = false) if (schema.Items is { } items) { - WalkItem("items", items, Walk, isComponent: false); + WalkItem("items", items, static (self, item, isComponent) => self.Walk(item, isComponent), isComponent: false); } if (schema.Not is { } not) { - WalkItem("not", not, Walk, isComponent: false); + WalkItem("not", not, static (self, item, isComponent) => self.Walk(item, isComponent), isComponent: false); } if (schema.AllOf is { Count: > 0 } allOf) { - WalkItem("allOf", allOf, Walk); + WalkItem("allOf", allOf, static (self, item) => self.Walk(item)); } if (schema.AnyOf is { Count: > 0 } anyOf) { - WalkItem("anyOf", anyOf, Walk); + WalkItem("anyOf", anyOf, static (self, item) => self.Walk(item)); } if (schema.OneOf is { Count: > 0 } oneOf) { - WalkItem("oneOf", oneOf, Walk); + WalkItem("oneOf", oneOf, static (self, item) => self.Walk(item)); } if (schema.Properties is { } properties) { - WalkDictionary("properties", properties, Walk); + WalkDictionary("properties", properties, static (self, item, isComponent) => self.Walk(item, isComponent)); } if (schema.AdditionalProperties is { } additionalProperties) { - WalkItem("additionalProperties", additionalProperties, Walk, isComponent: false); + WalkItem("additionalProperties", additionalProperties, static (self, item, isComponent) => self.Walk(item, isComponent), isComponent: false); } if (schema.Discriminator is { } discriminator) { - WalkItem("discriminator", discriminator, Walk); + WalkItem("discriminator", discriminator, static (self, item) => self.Walk(item)); } if (schema.ExternalDocs is { } externalDocs) { - WalkItem(OpenApiConstants.ExternalDocs, externalDocs, Walk); + WalkItem(OpenApiConstants.ExternalDocs, externalDocs, static (self, item) => self.Walk(item)); } Walk(schema as IOpenApiExtensible); @@ -992,7 +995,7 @@ internal void Walk(OpenApiDiscriminator? openApiDiscriminator) if (openApiDiscriminator.Mapping is { Count: > 0 } mapping) { - WalkDictionary("mapping", mapping, (item, _) => Walk((IOpenApiSchema)item)); + WalkDictionary("mapping", mapping, static (self, item, _) => self.Walk((IOpenApiSchema)item)); } } @@ -1016,7 +1019,7 @@ internal void Walk(IDictionary? examples) } _visitor.CurrentKeys.Example = example.Key; - WalkItem(example.Key, example.Value, Walk, isComponent: false); + WalkItem(example.Key, example.Value, static (self, item, isComponent) => self.Walk(item, isComponent), isComponent: false); _visitor.CurrentKeys.Example = null; } } @@ -1074,7 +1077,7 @@ internal void Walk(List examples) continue; } - WalkItem(i.ToString(), example, Walk, isComponent: false); + WalkItem(i.ToString(), example, static (self, item, isComponent) => self.Walk(item, isComponent), isComponent: false); } } @@ -1096,7 +1099,7 @@ internal void Walk(IList schemas) continue; } - WalkItem(i.ToString(), schema, Walk, isComponent: false); + WalkItem(i.ToString(), schema, static (self, item, isComponent) => self.Walk(item, isComponent), isComponent: false); } } @@ -1147,7 +1150,7 @@ internal void Walk(IDictionary? links) } _visitor.CurrentKeys.Link = item.Key; - WalkItem(item.Key, item.Value, Walk, isComponent: false); + WalkItem(item.Key, item.Value, static (self, item, isComponent) => self.Walk(item, isComponent), isComponent: false); _visitor.CurrentKeys.Link = null; } } @@ -1172,7 +1175,7 @@ internal void Walk(IOpenApiLink link, bool isComponent = false) if (link.Server is { } server) { - WalkItem(OpenApiConstants.Server, server, Walk); + WalkItem(OpenApiConstants.Server, server, static (self, item) => self.Walk(item)); } Walk(link as IOpenApiExtensible); @@ -1198,19 +1201,19 @@ internal void Walk(IOpenApiHeader header, bool isComponent = false) if (header.Content is { } content) { - WalkItem(OpenApiConstants.Content, content, Walk); + WalkItem(OpenApiConstants.Content, content, static (self, item) => self.Walk(item)); } if (header.Example is { } example) { - WalkItem(OpenApiConstants.Example, example, Walk); + WalkItem(OpenApiConstants.Example, example, static (self, item) => self.Walk(item)); } - WalkDictionary(OpenApiConstants.Examples, header.Examples, Walk); + WalkDictionary(OpenApiConstants.Examples, header.Examples, static (self, item, isComponent) => self.Walk(item, isComponent)); if (header.Schema is { } schema) { - WalkItem(OpenApiConstants.Schema, schema, Walk, isComponent: false); + WalkItem(OpenApiConstants.Schema, schema, static (self, item, isComponent) => self.Walk(item, isComponent), isComponent: false); } Walk(header as IOpenApiExtensible); @@ -1329,10 +1332,10 @@ private void Walk(string context, Action walk) /// An identifier for the context. /// The state to pass to the walk action. /// An action that walks objects within the context. - private void WalkItem(string context, T state, Action walk) + private void WalkItem(string context, T state, Action walk) { _visitor.Enter(context.Replace("/", "~1")); - walk(state); + walk(this, state); _visitor.Exit(); } @@ -1344,10 +1347,10 @@ private void WalkItem(string context, T state, Action walk) /// The state to pass to the walk action. /// Whether the state is a component. /// An action that walks objects within the context. - private void WalkItem(string context, T state, Action walk, bool isComponent) + private void WalkItem(string context, T state, Action walk, bool isComponent) { _visitor.Enter(context.Replace("/", "~1")); - walk(state, isComponent); + walk(this, state, isComponent); _visitor.Exit(); } @@ -1362,7 +1365,7 @@ private void WalkItem(string context, T state, Action walk, bool isC private void WalkDictionary( string context, IDictionary? state, - Action walk, + Action walk, bool isComponent = false) { if (state != null && state.Count > 0) @@ -1371,7 +1374,7 @@ private void WalkDictionary( foreach (var item in state) { - WalkItem(item.Key, (item.Value, isComponent), (state) => walk(state.Value, state.isComponent)); + WalkItem(item.Key, (this, item.Value, isComponent, walk), static (self, state) => state.walk(self, state.Value, state.isComponent)); } _visitor.Exit(); From fc91ab0b3aa9dcd4de64a3525a127a7b87c72d2b Mon Sep 17 00:00:00 2001 From: martincostello Date: Wed, 20 Aug 2025 13:37:20 +0100 Subject: [PATCH 5/9] Fix unused locals Resolve two CodeQL warnings. --- src/Microsoft.OpenApi/Services/OpenApiWalker.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.OpenApi/Services/OpenApiWalker.cs b/src/Microsoft.OpenApi/Services/OpenApiWalker.cs index e1603be8b..a00bf7b14 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiWalker.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiWalker.cs @@ -44,7 +44,7 @@ public void Walk(OpenApiDocument? doc) if (doc.Info is { } info) { - WalkItem(OpenApiConstants.Info, doc.Info, static (self, item) => self.Walk(item)); + WalkItem(OpenApiConstants.Info, info, static (self, item) => self.Walk(item)); } if (doc.Servers is { } servers) @@ -54,7 +54,7 @@ public void Walk(OpenApiDocument? doc) if (doc.Paths is { } paths) { - WalkItem(OpenApiConstants.Paths, doc.Paths, static (self, item) => self.Walk(item)); + WalkItem(OpenApiConstants.Paths, paths, static (self, item) => self.Walk(item)); } WalkDictionary(OpenApiConstants.Webhooks, doc.Webhooks, static (self, item, isComponent) => self.Walk(item, isComponent)); From 41c53c95c6affdfd75051d99377538cb09a84f65 Mon Sep 17 00:00:00 2001 From: martincostello Date: Wed, 20 Aug 2025 13:59:21 +0100 Subject: [PATCH 6/9] Address feedback - Create helpers to reduce code duplication. - Remove unused method. --- .../Services/OpenApiWalker.cs | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/src/Microsoft.OpenApi/Services/OpenApiWalker.cs b/src/Microsoft.OpenApi/Services/OpenApiWalker.cs index a00bf7b14..3f66d5515 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiWalker.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiWalker.cs @@ -97,7 +97,7 @@ internal void Walk(ISet? tags) // Visit tags if (tags is HashSet { Count: 1 } hashSet && hashSet.First() is { } only) { - WalkItem("0", only, static (self, item) => self.Walk(item)); + WalkItem("0", only, Walk); } else { @@ -110,10 +110,12 @@ internal void Walk(ISet? tags) } var context = index.ToString(); - WalkItem(context, tag, static (self, item) => self.Walk(item)); + WalkItem(context, tag, Walk); index++; } } + + static void Walk(OpenApiWalker self, OpenApiTag tag) => self.Walk(tag); } /// @@ -131,7 +133,7 @@ internal void Walk(ISet? tags) // Visit tags if (tags is HashSet { Count: 1 } hashSet && hashSet.First() is { } only) { - WalkItem("0", only, static (self, item) => self.Walk(item)); + WalkItem("0", only, Walk); } else { @@ -144,10 +146,12 @@ internal void Walk(ISet? tags) } var context = index.ToString(); - WalkItem(context, tag, static (self, item) => self.Walk(item)); + WalkItem(context, tag, Walk); index++; } } + + static void Walk(OpenApiWalker self, OpenApiTagReference tag) => self.Walk(tag); } /// @@ -1313,16 +1317,13 @@ internal void Walk(IOpenApiElement element) } } - /// - /// Adds a segment to the context path to enable pointing to the current location in the document - /// - /// An identifier for the context. - /// An action that walks objects within the context. - private void Walk(string context, Action walk) + private static string ReplaceSlashes(string value) { - _visitor.Enter(context.Replace("/", "~1")); - walk(); - _visitor.Exit(); +#if NET8_0_OR_GREATER + return value.Replace("/", "~1", StringComparison.Ordinal); +#else + return value.Replace("/", "~1"); +#endif } /// @@ -1334,7 +1335,7 @@ private void Walk(string context, Action walk) /// An action that walks objects within the context. private void WalkItem(string context, T state, Action walk) { - _visitor.Enter(context.Replace("/", "~1")); + _visitor.Enter(ReplaceSlashes(context)); walk(this, state); _visitor.Exit(); } @@ -1349,7 +1350,7 @@ private void WalkItem(string context, T state, Action walk) /// An action that walks objects within the context. private void WalkItem(string context, T state, Action walk, bool isComponent) { - _visitor.Enter(context.Replace("/", "~1")); + _visitor.Enter(ReplaceSlashes(context)); walk(this, state, isComponent); _visitor.Exit(); } @@ -1370,7 +1371,7 @@ private void WalkDictionary( { if (state != null && state.Count > 0) { - _visitor.Enter(context.Replace("/", "~1")); + _visitor.Enter(ReplaceSlashes(context)); foreach (var item in state) { From 70f4c87cb89e87c2bdcc5b5ee2721fb90ac315b1 Mon Sep 17 00:00:00 2001 From: martincostello Date: Wed, 20 Aug 2025 15:19:17 +0100 Subject: [PATCH 7/9] Refactor tag methods Create common implementation to walk `OpenApiTag` and `OpenApiTagReference`. --- .../Services/OpenApiWalker.cs | 70 +++++++------------ 1 file changed, 27 insertions(+), 43 deletions(-) diff --git a/src/Microsoft.OpenApi/Services/OpenApiWalker.cs b/src/Microsoft.OpenApi/Services/OpenApiWalker.cs index 3f66d5515..c1e0774d2 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiWalker.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiWalker.cs @@ -94,28 +94,7 @@ internal void Walk(ISet? tags) _visitor.Visit(tags); - // Visit tags - if (tags is HashSet { Count: 1 } hashSet && hashSet.First() is { } only) - { - WalkItem("0", only, Walk); - } - else - { - int index = 0; - foreach (var tag in tags) - { - if (tag is null) - { - continue; - } - - var context = index.ToString(); - WalkItem(context, tag, Walk); - index++; - } - } - - static void Walk(OpenApiWalker self, OpenApiTag tag) => self.Walk(tag); + WalkTags(tags, static (self, tag) => self.Walk(tag)); } /// @@ -131,27 +110,7 @@ internal void Walk(ISet? tags) _visitor.Visit(tags); // Visit tags - if (tags is HashSet { Count: 1 } hashSet && hashSet.First() is { } only) - { - WalkItem("0", only, Walk); - } - else - { - int index = 0; - foreach (var tag in tags) - { - if (tag is null) - { - continue; - } - - var context = index.ToString(); - WalkItem(context, tag, Walk); - index++; - } - } - - static void Walk(OpenApiWalker self, OpenApiTagReference tag) => self.Walk(tag); + WalkTags(tags, static (self, tag) => self.Walk(tag)); } /// @@ -1394,6 +1353,31 @@ private bool ProcessAsReference(IOpenApiReferenceHolder referenceableHolder, boo } return isReference; } + + private void WalkTags(ISet tags, Action walk) + where T : IOpenApiTag + { + // Visit tags + if (tags is HashSet { Count: 1 } hashSet && hashSet.First() is { } only) + { + WalkItem("0", only, walk); + } + else + { + int index = 0; + foreach (var tag in tags) + { + if (tag is null) + { + continue; + } + + var context = index.ToString(); + WalkItem(context, tag, walk); + index++; + } + } + } } /// From 1c4985517ad4c236ea797566d43c5de5e076de82 Mon Sep 17 00:00:00 2001 From: Martin Costello Date: Wed, 20 Aug 2025 15:20:26 +0100 Subject: [PATCH 8/9] Apply suggestions from code review Remove redundant comment. --- src/Microsoft.OpenApi/Services/OpenApiWalker.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Microsoft.OpenApi/Services/OpenApiWalker.cs b/src/Microsoft.OpenApi/Services/OpenApiWalker.cs index c1e0774d2..8b5868216 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiWalker.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiWalker.cs @@ -1357,7 +1357,6 @@ private bool ProcessAsReference(IOpenApiReferenceHolder referenceableHolder, boo private void WalkTags(ISet tags, Action walk) where T : IOpenApiTag { - // Visit tags if (tags is HashSet { Count: 1 } hashSet && hashSet.First() is { } only) { WalkItem("0", only, walk); From 09ba6822c7c7b6109b73b26166d2481d530acd37 Mon Sep 17 00:00:00 2001 From: martincostello Date: Wed, 20 Aug 2025 15:52:28 +0100 Subject: [PATCH 9/9] Update performance baseline Add results from 1c4985517ad4c236ea797566d43c5de5e076de82. --- .../performance.Descriptions-report-github.md | 22 +- .../performance.Descriptions-report.csv | 8 +- .../performance.Descriptions-report.html | 22 +- .../performance.Descriptions-report.json | 1183 +---------------- .../performance.EmptyModels-report-github.md | 70 +- .../performance.EmptyModels-report.csv | 58 +- .../performance.EmptyModels-report.html | 70 +- .../performance.EmptyModels-report.json | 2 +- 8 files changed, 127 insertions(+), 1308 deletions(-) diff --git a/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report-github.md b/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report-github.md index a507dd8e9..5c8d571e6 100644 --- a/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report-github.md +++ b/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report-github.md @@ -1,18 +1,18 @@ ``` -BenchmarkDotNet v0.14.0, Windows 11 (10.0.26100.3981) -11th Gen Intel Core i7-1185G7 3.00GHz, 1 CPU, 8 logical and 4 physical cores -.NET SDK 8.0.409 - [Host] : .NET 8.0.16 (8.0.1625.21506), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI - ShortRun : .NET 8.0.16 (8.0.1625.21506), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI +BenchmarkDotNet v0.15.2, Linux Ubuntu 24.04.2 LTS (Noble Numbat) +AMD EPYC 7763, 1 CPU, 4 logical and 2 physical cores +.NET SDK 8.0.413 + [Host] : .NET 8.0.19 (8.0.1925.36514), X64 RyuJIT AVX2 + ShortRun : .NET 8.0.19 (8.0.1925.36514), X64 RyuJIT AVX2 Job=ShortRun IterationCount=3 LaunchCount=1 WarmupCount=3 ``` -| Method | Mean | Error | StdDev | Gen0 | Gen1 | Gen2 | Allocated | -|------------- |-------------:|--------------:|-------------:|-----------:|-----------:|----------:|-------------:| -| PetStoreYaml | 470.3 μs | 138.05 μs | 7.57 μs | 58.5938 | 11.7188 | - | 380.53 KB | -| PetStoreJson | 166.0 μs | 43.84 μs | 2.40 μs | 39.0625 | 8.7891 | - | 242.67 KB | -| GHESYaml | 915,406.4 μs | 714,492.62 μs | 39,163.75 μs | 68000.0000 | 22000.0000 | 4000.0000 | 395800.98 KB | -| GHESJson | 470,609.4 μs | 264,698.88 μs | 14,509.04 μs | 42000.0000 | 15000.0000 | 3000.0000 | 257270.45 KB | +| Method | Mean | Error | StdDev | Gen0 | Gen1 | Gen2 | Allocated | +|------------- |---------------:|--------------:|------------:|-----------:|-----------:|----------:|-------------:| +| PetStoreYaml | 529.5 μs | 62.50 μs | 3.43 μs | 23.4375 | 3.9063 | - | 387.26 KB | +| PetStoreJson | 240.8 μs | 15.69 μs | 0.86 μs | 13.6719 | 1.9531 | - | 249.1 KB | +| GHESYaml | 1,097,576.6 μs | 100,584.42 μs | 5,513.37 μs | 26000.0000 | 20000.0000 | 3000.0000 | 384492.38 KB | +| GHESJson | 516,328.2 μs | 87,964.22 μs | 4,821.62 μs | 16000.0000 | 9000.0000 | 2000.0000 | 245957.5 KB | diff --git a/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.csv b/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.csv index e6299ad40..1a21885c7 100644 --- a/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.csv +++ b/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.csv @@ -1,5 +1,5 @@ Method,Job,AnalyzeLaunchVariance,EvaluateOverhead,MaxAbsoluteError,MaxRelativeError,MinInvokeCount,MinIterationTime,OutlierMode,Affinity,EnvironmentVariables,Jit,LargeAddressAware,Platform,PowerPlanMode,Runtime,AllowVeryLargeObjects,Concurrent,CpuGroups,Force,HeapAffinitizeMask,HeapCount,NoAffinitize,RetainVm,Server,Arguments,BuildConfiguration,Clock,EngineFactory,NuGetReferences,Toolchain,IsMutator,InvocationCount,IterationCount,IterationTime,LaunchCount,MaxIterationCount,MaxWarmupIterationCount,MemoryRandomization,MinIterationCount,MinWarmupIterationCount,RunStrategy,UnrollFactor,WarmupCount,Mean,Error,StdDev,Gen0,Gen1,Gen2,Allocated -PetStoreYaml,ShortRun,False,Default,Default,Default,Default,Default,Default,11111111,Empty,RyuJit,Default,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,470.3 μs,138.05 μs,7.57 μs,58.5938,11.7188,0.0000,380.53 KB -PetStoreJson,ShortRun,False,Default,Default,Default,Default,Default,Default,11111111,Empty,RyuJit,Default,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,166.0 μs,43.84 μs,2.40 μs,39.0625,8.7891,0.0000,242.67 KB -GHESYaml,ShortRun,False,Default,Default,Default,Default,Default,Default,11111111,Empty,RyuJit,Default,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,"915,406.4 μs","714,492.62 μs","39,163.75 μs",68000.0000,22000.0000,4000.0000,395800.98 KB -GHESJson,ShortRun,False,Default,Default,Default,Default,Default,Default,11111111,Empty,RyuJit,Default,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,"470,609.4 μs","264,698.88 μs","14,509.04 μs",42000.0000,15000.0000,3000.0000,257270.45 KB +PetStoreYaml,ShortRun,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,Default,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,529.5 μs,62.50 μs,3.43 μs,23.4375,3.9063,0.0000,387.26 KB +PetStoreJson,ShortRun,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,Default,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,240.8 μs,15.69 μs,0.86 μs,13.6719,1.9531,0.0000,249.1 KB +GHESYaml,ShortRun,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,Default,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,"1,097,576.6 μs","100,584.42 μs","5,513.37 μs",26000.0000,20000.0000,3000.0000,384492.38 KB +GHESJson,ShortRun,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,Default,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,"516,328.2 μs","87,964.22 μs","4,821.62 μs",16000.0000,9000.0000,2000.0000,245957.5 KB diff --git a/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.html b/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.html index 4f578eca3..5661bcdf8 100644 --- a/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.html +++ b/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.html @@ -2,7 +2,7 @@ -performance.Descriptions-20250514-154213 +performance.Descriptions-20250820-142630