Skip to content

Comments

perf: optimize parsing V3.1 documents#2748

Open
nikcio wants to merge 5 commits intomicrosoft:mainfrom
nikcio:feature/perf-parsing
Open

perf: optimize parsing V3.1 documents#2748
nikcio wants to merge 5 commits intomicrosoft:mainfrom
nikcio:feature/perf-parsing

Conversation

@nikcio
Copy link

@nikcio nikcio commented Feb 22, 2026

Pull Request

Description

Optimizes ParsingContext.GetLocation() and its usage in OpenApiSchemaDeserializer only loading the location when needed.

Also adds GHES Next descriptions to performance tests to measure impact in V3.1 documents.

Measured baseline before change:

BenchmarkDotNet v0.15.8, Windows 11 (10.0.26200.7840/25H2/2025Update/HudsonValley2)
AMD Ryzen 7 7800X3D 4.20GHz, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.418
  [Host]   : .NET 8.0.24 (8.0.24, 8.0.2426.7010), X64 RyuJIT x86-64-v4
  ShortRun : .NET 8.0.24 (8.0.24, 8.0.2426.7010), X64 RyuJIT x86-64-v4

Job=ShortRun  IterationCount=3  LaunchCount=1  
WarmupCount=3  

| Method       | Mean           | Error           | StdDev       | Gen0        | Gen1       | Gen2      | Allocated     |
|------------- |---------------:|----------------:|-------------:|------------:|-----------:|----------:|--------------:|
| PetStoreYaml |       279.0 μs |       272.39 μs |     14.93 μs |      5.8594 |          - |         - |     361.38 KB |
| PetStoreJson |       102.9 μs |        17.98 μs |      0.99 μs |      4.3945 |     0.9766 |         - |     223.52 KB |
| GHESYaml     |   635,487.8 μs |   279,254.76 μs | 15,306.90 μs |   9000.0000 |  8000.0000 | 2000.0000 |  345336.55 KB |
| GHESJson     |   277,064.1 μs |   164,049.38 μs |  8,992.10 μs |   4000.0000 |  3000.0000 | 1000.0000 |  206858.06 KB |
| GHESNextYaml | 4,479,297.6 μs | 1,561,306.63 μs | 85,580.48 μs | 191000.0000 | 11000.0000 | 3000.0000 | 9268440.43 KB |
| GHESNextJson | 3,305,679.1 μs |   679,209.31 μs | 37,229.75 μs | 186000.0000 | 17000.0000 | 1000.0000 |  9133635.6 KB |

Measured after change:

BenchmarkDotNet v0.15.8, Windows 11 (10.0.26200.7840/25H2/2025Update/HudsonValley2)
AMD Ryzen 7 7800X3D 4.20GHz, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.418
  [Host]   : .NET 8.0.24 (8.0.24, 8.0.2426.7010), X64 RyuJIT x86-64-v4
  ShortRun : .NET 8.0.24 (8.0.24, 8.0.2426.7010), X64 RyuJIT x86-64-v4

Job=ShortRun  IterationCount=3  LaunchCount=1  
WarmupCount=3  

| Method       | Mean         | Error           | StdDev       | Gen0       | Gen1      | Gen2      | Allocated    |
|------------- |-------------:|----------------:|-------------:|-----------:|----------:|----------:|-------------:|
| PetStoreYaml |     266.8 μs |       423.33 μs |     23.20 μs |     5.8594 |         - |         - |    361.38 KB |
| PetStoreJson |     104.9 μs |        38.83 μs |      2.13 μs |     4.3945 |    0.9766 |         - |    223.52 KB |
| GHESYaml     | 627,326.7 μs |   525,719.23 μs | 28,816.45 μs |  9000.0000 | 8000.0000 | 2000.0000 | 345336.55 KB |
| GHESJson     | 256,258.8 μs |   156,676.06 μs |  8,587.94 μs |  4000.0000 | 3000.0000 | 1000.0000 | 206858.38 KB |
| GHESNextYaml | 840,612.7 μs | 1,063,489.38 μs | 58,293.44 μs | 20000.0000 | 9000.0000 | 2000.0000 | 908819.18 KB |
| GHESNextJson | 478,093.3 μs |    98,044.22 μs |  5,374.13 μs | 16000.0000 | 7000.0000 | 1000.0000 | 774015.55 KB |

Type of Change

  • 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 not work as expected)
  • Documentation update
  • Other (please describe): Performance

Related Issue(s)

Changes Made

  • Use StringBuilder in ParsingContext.GetLocation()
  • Only load nodeLocation when needed in OpenApiSchemaDeserializer

Testing

  • Unit tests added/updated
  • Integration tests added/updated
  • Performance tests added/updated
  • Manual testing performed
  • All existing tests pass

Checklist

  • My code follows the code style of this project
  • I have performed a self-review of my own code
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

Versions applicability

  • My change applies to the version 1.X of the library, if so PR link:
  • My change applies to the version 2.X of the library, if so PR link:
  • My change applies to the version 3.X of the library, if so PR link: (This PR)
  • I have evaluated the applicability of my change against the other versions above.

See the contributing guidelines for more information about how patches are applied across multiple versions.

Additional Notes

@nikcio nikcio requested a review from a team as a code owner February 22, 2026 17:38
Copilot AI review requested due to automatic review settings February 22, 2026 17:38
@nikcio
Copy link
Author

nikcio commented Feb 22, 2026

@microsoft-github-policy-service agree

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Optimizes OpenAPI v3.1/v3.2 schema parsing by avoiding JSON pointer location construction unless it’s needed for $ref handling, and extends the benchmark suite to include “GHES Next” OpenAPI descriptions to measure performance impact on larger v3.1 documents.

Changes:

  • Lazily compute ParsingContext.GetLocation() only when a schema $ref pointer is present (v3.1/v3.2 schema deserializers).
  • Optimize ParsingContext.GetLocation() implementation to reduce allocations (StringBuilder-based pointer construction).
  • Add GHES Next YAML/JSON benchmarks and update committed BenchmarkDotNet artifact reports.

Reviewed changes

Copilot reviewed 11 out of 12 changed files in this pull request and generated no comments.

Show a summary per file
File Description
src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs Avoids computing location unless $ref is present.
src/Microsoft.OpenApi/Reader/V32/OpenApiSchemaDeserializer.cs Avoids computing location unless $ref is present.
src/Microsoft.OpenApi/Reader/ParsingContext.cs Optimizes JSON pointer location string construction.
performance/benchmark/Descriptions.cs Adds GHES Next YAML/JSON benchmarks and setup downloads.
performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.EmptyModels-report.html Updated benchmark output artifact.
performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.EmptyModels-report.csv Updated benchmark output artifact.
performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.EmptyModels-report-github.md Updated benchmark output artifact.
performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.json Updated benchmark output artifact (includes GHES Next results).
performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.html Updated benchmark output artifact (includes GHES Next results).
performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.csv Updated benchmark output artifact (includes GHES Next results).
performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report-github.md Updated benchmark output artifact (includes GHES Next results).

Copy link
Member

@baywet baywet left a comment

Choose a reason for hiding this comment

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

Thanks for the contribution!

var segment = segments[i];

// Escape ~ and / per RFC 6901
if (segment.Contains("~") || segment.Contains("/"))
Copy link
Member

Choose a reason for hiding this comment

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

We could improve this further by using the char overload and passing a string comparison when the target is net standard 2.1 or greater, etc... (using conditional compilation).
(see this example )

Copy link
Author

Choose a reason for hiding this comment

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

@baywet I have now tried this locally and it didn't make much of a difference. I also found a more performant solution instead see: #2748 (comment)

But let me know if you'd rather like the StringBuilder implementation for better readability with the char overload added, then I'll update the PR 😄

Comment on lines 198 to 201
if (i > 0)
{
sb.Append('/');
}
Copy link
Member

Choose a reason for hiding this comment

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

I don't believe the current implementation terminates with a slash.

Copy link
Author

Choose a reason for hiding this comment

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

Hi @baywet, maybe I'm misunderstanding, but the code shouldn't be terminating with a slash. I've added some tests to verify that ParsingContextTests.cs let me know if that is overkill 😄

return "#/";
}

var sb = new StringBuilder("#/");
Copy link
Member

Choose a reason for hiding this comment

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

nit: we technically could use a span instead since we kind of know the size in advance. Would that yield any true performance benefits?
Also this would probably need to be conditional because of the target frameworks.

Copy link
Author

@nikcio nikcio Feb 23, 2026

Choose a reason for hiding this comment

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

Hi @baywet, I've tried creating a more performant implementation now it should also be much better for allocation. Let me know what version you prefer 😄

For reference I did some local bench marking of the 3 ways of doing the GetLocation():

ArrayPool (Current implementation)
StringBuilder (Initial implementation with the suggested char overload)
LinqJoin (Original implementation)

BenchmarkDotNet v0.15.8, Windows 11 (10.0.26200.7840/25H2/2025Update/HudsonValley2)
AMD Ryzen 7 7800X3D 4.20GHz, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.418
  [Host]   : .NET 8.0.24 (8.0.24, 8.0.2426.7010), X64 RyuJIT x86-64-v4
  ShortRun : .NET 8.0.24 (8.0.24, 8.0.2426.7010), X64 RyuJIT x86-64-v4

Job=ShortRun  IterationCount=3  LaunchCount=1
WarmupCount=3

| Method        | Scenario         | Mean        | Error       | StdDev    | Ratio  | RatioSD | Gen0   | Allocated | Alloc Ratio |
|-------------- |----------------- |------------:|------------:|----------:|-------:|--------:|-------:|----------:|------------:|
| ArrayPool     | Empty            |   0.2657 ns |   1.2196 ns | 0.0669 ns |   1.05 |    0.34 |      - |         - |          NA |
| StringBuilder | Empty            |   0.1835 ns |   1.8641 ns | 0.1022 ns |   0.72 |    0.40 |      - |         - |          NA |
| LinqJoin      | Empty            |  49.7601 ns |   0.1247 ns | 0.0068 ns | 196.25 |   46.62 | 0.0020 |     104 B |          NA |
|               |                  |             |             |           |        |         |        |           |             |
| ArrayPool     | Long             | 107.2965 ns |  35.4506 ns | 1.9432 ns |   1.00 |    0.02 | 0.0055 |     280 B |        1.00 |
| StringBuilder | Long             | 148.4526 ns |  71.0306 ns | 3.8934 ns |   1.38 |    0.04 | 0.0162 |     824 B |        2.94 |
| LinqJoin      | Long             | 374.3515 ns |  88.2121 ns | 4.8352 ns |   3.49 |    0.07 | 0.0219 |    1120 B |        4.00 |
|               |                  |             |             |           |        |         |        |           |             |
| ArrayPool     | Short            |  41.2347 ns |  10.4656 ns | 0.5737 ns |   1.00 |    0.02 | 0.0020 |     104 B |        1.00 |
| StringBuilder | Short            |  40.0271 ns |   9.2822 ns | 0.5088 ns |   0.97 |    0.02 | 0.0041 |     208 B |        2.00 |
| LinqJoin      | Short            | 141.3804 ns |  33.8513 ns | 1.8555 ns |   3.43 |    0.06 | 0.0081 |     416 B |        4.00 |
|               |                  |             |             |           |        |         |        |           |             |
| ArrayPool     | WithSpecialChars |  69.0026 ns |  24.3098 ns | 1.3325 ns |   1.00 |    0.02 | 0.0035 |     176 B |        1.00 |
| StringBuilder | WithSpecialChars | 132.8347 ns |  31.1054 ns | 1.7050 ns |   1.93 |    0.04 | 0.0119 |     608 B |        3.45 |
| LinqJoin      | WithSpecialChars | 244.7669 ns | 105.0945 ns | 5.7606 ns |   3.55 |    0.09 | 0.0157 |     800 B |        4.55 |

For reference the test data:

"Empty" => new Stack<string>(),
"Short" => BuildStack(["paths", "pets", "get"]),
"Long" => BuildStack(["components", "schemas", "Pet", "properties", "name", "type", "string", "minLength", "1", "maxLength"]),
"WithSpecialChars" => BuildStack(["paths", "/pets/{id}", "get", "responses", "200~ok"]),

Also the results of the descriptions benchmark now looks like:

BenchmarkDotNet v0.15.8, Windows 11 (10.0.26200.7840/25H2/2025Update/HudsonValley2)
AMD Ryzen 7 7800X3D 4.20GHz, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.418
  [Host]   : .NET 8.0.24 (8.0.24, 8.0.2426.7010), X64 RyuJIT x86-64-v4
  ShortRun : .NET 8.0.24 (8.0.24, 8.0.2426.7010), X64 RyuJIT x86-64-v4

Job=ShortRun  IterationCount=3  LaunchCount=1  
WarmupCount=3  

| Method       | Mean         | Error         | StdDev       | Gen0       | Gen1      | Gen2      | Allocated    |
|------------- |-------------:|--------------:|-------------:|-----------:|----------:|----------:|-------------:|
| PetStoreYaml |     261.1 μs |     105.89 μs |      5.80 μs |     5.8594 |         - |         - |    361.38 KB |
| PetStoreJson |     101.9 μs |      48.20 μs |      2.64 μs |     4.3945 |    0.9766 |         - |    223.52 KB |
| GHESYaml     | 602,932.7 μs | 170,410.86 μs |  9,340.79 μs |  9000.0000 | 8000.0000 | 2000.0000 | 345336.55 KB |
| GHESJson     | 254,976.7 μs | 111,875.43 μs |  6,132.27 μs |  4000.0000 | 3000.0000 | 1000.0000 | 206858.06 KB |
| GHESNextYaml | 729,602.0 μs | 357,122.29 μs | 19,575.08 μs | 13000.0000 | 9000.0000 | 2000.0000 | 541566.37 KB |
| GHESNextJson | 378,208.4 μs | 109,458.45 μs |  5,999.79 μs |  8000.0000 | 5000.0000 | 1000.0000 | 406762.41 KB |

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.

2 participants